diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/mozbase/mozpower | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/mozbase/mozpower')
19 files changed, 2617 insertions, 0 deletions
diff --git a/testing/mozbase/mozpower/mozpower/__init__.py b/testing/mozbase/mozpower/mozpower/__init__.py new file mode 100644 index 0000000000..6ac10d076b --- /dev/null +++ b/testing/mozbase/mozpower/mozpower/__init__.py @@ -0,0 +1,24 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from .intel_power_gadget import ( + IPGEmptyFileError, + IPGMissingOutputFileError, + IPGTimeoutError, + IPGUnknownValueTypeError, +) +from .mozpower import MissingProcessorInfoError, MozPower, OsCpuComboMissingError +from .powerbase import IPGExecutableMissingError, PlatformUnsupportedError + +__all__ = [ + "IPGEmptyFileError", + "IPGExecutableMissingError", + "IPGMissingOutputFileError", + "IPGTimeoutError", + "IPGUnknownValueTypeError", + "MissingProcessorInfoError", + "MozPower", + "OsCpuComboMissingError", + "PlatformUnsupportedError", +] diff --git a/testing/mozbase/mozpower/mozpower/intel_power_gadget.py b/testing/mozbase/mozpower/mozpower/intel_power_gadget.py new file mode 100644 index 0000000000..0382084d8f --- /dev/null +++ b/testing/mozbase/mozpower/mozpower/intel_power_gadget.py @@ -0,0 +1,910 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import csv +import os +import re +import subprocess +import threading +import time + +from .mozpowerutils import get_logger + + +class IPGTimeoutError(Exception): + """IPGTimeoutError is raised when we cannot stop Intel Power + Gadget from running. One possble cause of this is not calling + `stop_ipg` through a `finalize_power_measurements` call. The + other possiblity is that IPG failed to stop. + """ + + pass + + +class IPGMissingOutputFileError(Exception): + """IPGMissingOutputFile is raised when a file path is given + to _clean_ipg_file but it does not exist or cannot be found + at the expected location. + """ + + pass + + +class IPGEmptyFileError(Exception): + """IPGEmptyFileError is raised when a file path is given + to _clean_ipg_file and it exists but it is empty (contains + no results to clean). + """ + + pass + + +class IPGUnknownValueTypeError(Exception): + """IPGUnknownValueTypeError is raised when a value within a + given results file (that was cleaned) cannot be converted to + its column's expected data type. + """ + + pass + + +class IntelPowerGadget(object): + """IntelPowerGadget provides methods for using Intel Power Gadget + to measure power consumption. + + :: + + from mozpower.intel_power_gadget import IntelPowerGadget + + # On Mac, the ipg_exe_path should point to '/Applications/Intel Power Gadget/PowerLog' + ipg = IntelPowerGadget(ipg_exe_path, ipg_measure_duration=600, sampling_rate=500) + + ipg.start_ipg() + + # Run tests... + + ipg.stop_ipg() + + # Now process results with IPGResultsHandler by passing it + # ipg.output_files, ipg.output_file_ext, ipg.ipg_measure_duration, + # and ipg.sampling_rate. + """ + + def __init__( + self, + exe_file_path, + ipg_measure_duration=10, + sampling_rate=1000, + output_file_ext=".txt", + file_counter=1, + output_file_path="powerlog", + logger_name="mozpower", + ): + """Initializes the IntelPowerGadget object. + + :param str exe_file_path: path to Intel Power Gadget 'PowerLog' executable. + :param int ipg_measure_duration: duration to run IPG for in seconds. + This does not dictate when the tools shuts down. It only stops when stop_ipg + is called in case the test runs for a non-deterministic amount of time. The + IPG executable requires a duration to be supplied. The machinery in place is + to ensure that IPG keeps recording as long as the experiment runs, so + multiple files may result, which is handled in + IPGResultsHandler._combine_cumulative_rows. Defaults to 10s. + :param int sampling_rate: sampling rate of measurements in milliseconds. + Defaults to 1000ms. + :param output_file_ext: file extension of data being output. Defaults to '.txt'. + :param int file_counter: dictates the start of the file numbering (used + when test time exceeds duration value). Defaults to 0. + :param str output_file_path: path to the output location combined + with the output file prefix. Defaults to current working directory using the + prefix 'powerlog'. + :param str logger_name: logging logger name. Defaults to 'mozpower'. + """ + self._logger = get_logger(logger_name) + + # Output-specific settings + self._file_counter = file_counter + self._output_files = [] + self._output_file_path = output_file_path + self._output_file_ext = output_file_ext + self._output_dir_path, self._output_file_prefix = os.path.split( + self._output_file_path + ) + + # IPG-specific settings + self._ipg_measure_duration = ipg_measure_duration # in seconds + self._sampling_rate = sampling_rate # in milliseconds + self._exe_file_path = exe_file_path + + # Setup thread for measurement gathering + self._thread = threading.Thread( + target=self.run, args=(exe_file_path, ipg_measure_duration) + ) + self._thread.daemon = True + self._running = False + + @property + def output_files(self): + """Returns the list of files produced from running IPG. + + :returns: list + """ + return self._output_files + + @property + def output_file_ext(self): + """Returns the extension of the files produced by IPG. + + :returns: str + """ + return self._output_file_ext + + @property + def output_file_prefix(self): + """Returns the prefix of the files produces by IPG. + + :returns: str + """ + return self._output_file_prefix + + @property + def output_dir_path(self): + """Returns the output directory of the files produced by IPG. + + :returns: str + """ + return self._output_dir_path + + @property + def sampling_rate(self): + """Returns the specified sampling rate. + + :returns: int + """ + return self._sampling_rate + + @property + def ipg_measure_duration(self): + """Returns the IPG measurement duration (see __init__ for description + of what this value does). + + :returns: int + """ + return self._ipg_measure_duration + + def start_ipg(self): + """Starts the thread which runs IPG to start gathering measurements.""" + self._logger.info("Starting IPG thread") + self._stop = False + self._thread.start() + + def stop_ipg(self, wait_interval=10, timeout=200): + """Stops the thread which runs IPG and waits for it to finish it's work. + + :param int wait_interval: interval time (in seconds) at which to check if + IPG is finished. + :param int timeout: time to wait until _wait_for_ipg hits a time out, + in seconds. + """ + self._logger.info("Stopping IPG thread") + self._stop = True + self._wait_for_ipg(wait_interval=wait_interval, timeout=timeout) + + def _wait_for_ipg(self, wait_interval=10, timeout=200): + """Waits for IPG to finish it's final dataset. + + :param int wait_interval: interval time (in seconds) at which to check if + IPG is finished. + :param int timeout: time to wait until this method hits a time out, + in seconds. + :raises: * IPGTimeoutError + """ + timeout_stime = time.time() + while self._running and (time.time() - timeout_stime) < timeout: + self._logger.info( + "Waiting %s sec for Intel Power Gadget to stop" % wait_interval + ) + time.sleep(wait_interval) + if self._running: + raise IPGTimeoutError("Timed out waiting for IPG to stop") + + def _get_output_file_path(self): + """Returns the next output file name to be used. Starts at 1 and increases + at each successive call. Used when the test duration exceeds the specified + duration through ipg_meaure_duration. + + :returns: str + """ + self._output_file_path = os.path.join( + self._output_dir_path, + "%s_%s_%s" + % (self._output_file_prefix, self._file_counter, self._output_file_ext), + ) + self._file_counter += 1 + self._logger.info( + "Creating new file for IPG data measurements: %s" % self._output_file_path + ) + return self._output_file_path + + def run(self, exe_file_path, ipg_measure_duration): + """Runs the IPG measurement gatherer. While stop has not been set to True, + it continuously gathers measurements into separate files that are merged + by IPGResultsHandler once the output_files are passed to it. + + :param str exe_file_path: file path of where to find the IPG executable. + :param int ipg_measure_duration: time to gather measurements for. + """ + self._logger.info("Starting to gather IPG measurements in thread") + self._running = True + + while not self._stop: + outname = self._get_output_file_path() + self._output_files.append(outname) + + try: + subprocess.check_output( + [ + exe_file_path, + "-duration", + str(ipg_measure_duration), + "-resolution", + str(self._sampling_rate), + "-file", + outname, + ], + stderr=subprocess.STDOUT, + ) + except subprocess.CalledProcessError as e: + error_log = str(e) + if e.output: + error_log = e.output.decode() + self._logger.critical( + "Error while running Intel Power Gadget: %s" % error_log + ) + + self._running = False + + +class IPGResultsHandler(object): + """IPGResultsHandler provides methods for cleaning and formatting + the files that were created by IntelPowerGadget. + + :: + + from mozpower.intel_power_gadget import IntelPowerGadget, IPGResultsHandler + + ipg = IntelPowerGadget(ipg_exe_path, ipg_measure_duration=600, sampling_rate=500) + + # Run tests - call start_ipg and stop_ipg... + + ipg_rh = IPGResultsHandler( + ipg.output_files, + ipg.output_dir_path, + ipg_measure_duration=ipg.ipg_measure_duration, + sampling_rate=ipg.sampling_rate + ) + + cleaned_data = ipg_rh.clean_ipg_data() + # you can also get the data from results after calling clean_ipg_data + cleaned_data = ipg_rh.results + + perfherder_data = ipg_rh.format_ipg_data_to_partial_perfherder( + experiment_duration, + test_name + ) + # you can also get the perfherder data from summarized_results + # after calling format_ipg_data_to_partial_perfherder + perfherder_data = ipg_rh.summarized_results + """ + + def __init__( + self, + output_files, + output_dir_path, + ipg_measure_duration=10, + sampling_rate=1000, + logger_name="mozpower", + ): + """Initializes the IPGResultsHandler object. + + :param list output_files: files output by IntelPowerGadget containing + the IPG data. + :param str output_dir_path: location to store cleaned files and merged data. + :param int ipg_measure_duration: length of time that each IPG measurement lasted + in seconds (see IntelPowerGadget for more information on this argument). + Defaults to 10s. + :param int sampling_rate: sampling rate of the measurements in milliseconds. + Defaults to 1000ms. + :param str logger_name: logging logger name. Defaults to 'mozpower'. + """ + self._logger = get_logger(logger_name) + self._results = {} + self._summarized_results = {} + self._cleaned_files = [] + self._csv_header = None + self._merged_output_path = None + self._output_file_prefix = None + self._output_file_ext = None + + self._sampling_rate = sampling_rate + self._ipg_measure_duration = ipg_measure_duration + self._output_files = output_files + self._output_dir_path = output_dir_path + + if self._output_files: + # Gather the output file extension, and prefix + # for the cleaned files, and the merged file. + single_file = self._output_files[0] + _, file = os.path.split(single_file) + self._output_file_ext = "." + file.split(".")[-1] + + # This prefix detection depends on the path names created + # by _get_output_file_path. + integer_re = re.compile(r"""(.*)_[\d+]_%s""" % self._output_file_ext) + match = integer_re.match(file) + if match: + self._output_file_prefix = match.group(1) + else: + self._output_file_prefix = file.split("_")[0] + self._logger.warning( + "Cannot find output file prefix from output file name %s" + "using the following prefix: %s" % (file, self._output_file_prefix) + ) + + @property + def results(self): + """Returns the cleaned IPG data in the form of a dict. + Each key is a measurement name with the values being the list + of measurements. All value lists are sorted in increasing time. + + :returns: dict + """ + return self._results + + @property + def summarized_results(self): + """Returns IPG data in the form of a dict that is formatted + into a perfherder data blob. + + :returns: dict + """ + return self._summarized_results + + @property + def cleaned_files(self): + """Returns a list of cleaned IPG data files that were output + after running clean_ipg_data. + + :returns: list + """ + return self._cleaned_files + + @property + def merged_output_path(self): + """Returns the path to the cleaned, and merged output file. + + :returns: str + """ + return self._merged_output_path + + def _clean_ipg_file(self, file): + """Cleans an individual IPG file and writes out a cleaned file. + + An uncleaned file looks like this (this contains a partial + sample of the time series data): + ``` + "System Time","RDTSC","Elapsed Time (sec)" + "12:11:05:769","61075218263548"," 2.002" + "12:11:06:774","61077822279584"," 3.007" + "12:11:07:778","61080424421708"," 4.011" + "12:11:08:781","61083023535972"," 5.013" + "12:11:09:784","61085623402302"," 6.016" + + "Total Elapsed Time (sec) = 10.029232" + "Measured RDTSC Frequency (GHz) = 2.592" + + "Cumulative Processor Energy_0 (Joules) = 142.337524" + "Cumulative Processor Energy_0 (mWh) = 39.538201" + "Average Processor Power_0 (Watt) = 14.192265" + + "Cumulative IA Energy_0 (Joules) = 121.888000" + "Cumulative IA Energy_0 (mWh) = 33.857778" + "Average IA Power_0 (Watt) = 12.153273" + + "Cumulative DRAM Energy_0 (Joules) = 7.453308" + "Cumulative DRAM Energy_0 (mWh) = 2.070363" + "Average DRAM Power_0 (Watt) = 0.743158" + + "Cumulative GT Energy_0 (Joules) = 0.079834" + "Cumulative GT Energy_0 (mWh) = 0.022176" + "Average GT Power_0 (Watt) = 0.007960" + ``` + + While a cleaned file looks like: + ``` + System Time,RDTSC,Elapsed Time (sec) + 12:11:05:769,61075218263548, 2.002 + 12:11:06:774,61077822279584, 3.007 + 12:11:07:778,61080424421708, 4.011 + 12:11:08:781,61083023535972, 5.013 + 12:11:09:784,61085623402302, 6.016 + ``` + + The first portion of the file before the '"Total Elapsed Time' entry is + considered as the time series which is captured as a dict in the returned + results. + + All the text starting from '"Total Elapsed Time' is removed from the file and + is considered as a summary of the experiment (which is returned). This + portion is ignored in any other processing done by IPGResultsHandler, + and is not saved, unlike the returned results value. + + Note that the new lines are removed from the summary that is returned + (as well as the results). + + :param str file: file to clean. + :returns: a tuple of (dict, list, str) for + (results, summary, clean_output_path) + see method comments above for information on what + this output contains. + :raises: * IPGMissingOutputFileError + * IPGEmptyFileError + """ + self._logger.info("Cleaning IPG data file %s" % file) + + txt = "" + if os.path.exists(file): + with open(file, "r") as f: + txt = f.read() + else: + # This should never happen, so prevent IPGResultsHandler + # from continuing to clean files if it does occur. + raise IPGMissingOutputFileError( + "The following file does not exist so it cannot be cleaned: %s " % file + ) + + if txt == "": + raise IPGEmptyFileError( + "The following file is empty so it cannot be cleaned: %s" % file + ) + + # Split the time series data from the summary + tseries, summary = re.split('"Total Elapsed Time', txt) + + # Clean the summary + summary = '"Total Elapsed Time' + summary + summary = [ + line for line in re.split(r"""\n""", summary.replace("\r", "")) if line + ] + + # Clean the time series data, store the clean rows to write out later, + # and format the rows into a dict entry for each measure. + results = {} + clean_rows = [] + csv_header = None + for c, row in enumerate( + csv.reader(tseries.split("\n"), quotechar=str('"'), delimiter=str(",")) + ): + if not row: + continue + + # Make sure we don't have any bad line endings + # contaminating the cleaned rows. + fmt_row = [ + val.replace("\n", "") + .replace("\t", "") + .replace("\r", "") + .replace("\\n", "") + .strip() + for val in row + ] + + if not fmt_row or not any(fmt_row): + continue + + clean_rows.append(fmt_row) + if c == 0: + csv_header = fmt_row + for col in fmt_row: + results[col] = [] + continue + for i, col in enumerate(fmt_row): + results[csv_header[i]].append(col) + + # Write out the cleaned data and check to make sure + # the csv header hasn't changed mid-experiment + _, fname = os.path.split(file) + clean_output_path = os.path.join( + self._output_dir_path, + fname.replace( + self._output_file_ext, + "_clean.%s" % self._output_file_ext.replace(".", ""), + ), + ) + self._logger.info("Writing cleaned IPG results to %s" % clean_output_path) + try: + with open(clean_output_path, "w") as csvfile: + writer = csv.writer(csvfile) + for count, row in enumerate(clean_rows): + if count == 0: + if self._csv_header is None: + self._csv_header = row + elif self._csv_header != row: + self._logger.warning( + "CSV Headers from IPG data have changed during the experiment " + "expected: %s; got: %s" + % (str(self._csv_header), str(row)) + ) + writer.writerow(row) + except Exception as e: + self._logger.warning( + "Could not write out cleaned results of %s to %s due to the following error" + ", skipping this step: %s" % (file, clean_output_path, str(e)) + ) + + # Check to make sure the expected number of samples + # exist in the file and that the columns have the correct + # data type. + column_datatypes = {"System Time": str, "RDTSC": int, "default": float} + # pylint --py3k W1619 + expected_samples = int( + self._ipg_measure_duration / (float(self._sampling_rate) / 1000) + ) + for key in results: + if len(results[key]) != expected_samples: + self._logger.warning( + "Unexpected number of samples in %s for column %s - " + "expected: %s, got: %s" + % (clean_output_path, key, expected_samples, len(results[key])) + ) + + dtype = column_datatypes["default"] + if key in column_datatypes: + dtype = column_datatypes[key] + + for val in results[key]: + try: + # Check if converting from a string to its expected data + # type works, if not, it's not the expected data type. + dtype(val) + except ValueError as e: + raise IPGUnknownValueTypeError( + "Cleaned file %s entry %s in column %s has unknown type " + "instead of the expected type %s - data corrupted, " + "cannot continue: %s" + % (clean_output_path, str(val), key, dtype.__name__, str(e)) + ) + + # Check to make sure that IPG measured for the expected + # amount of time. + etime = "Elapsed Time (sec)" + if etime in results: + total_time = int(float(results[etime][-1])) + if total_time != self._ipg_measure_duration: + self._logger.warning( + "Elapsed time found in file %s is different from expected length - " + "expected: %s, got: %s" + % (clean_output_path, self._ipg_measure_duration, total_time) + ) + else: + self._logger.warning( + "Missing 'Elapsed Time (sec)' in file %s" % clean_output_path + ) + + return results, summary, clean_output_path + + def _combine_cumulative_rows(self, cumulatives): + """Combine cumulative rows from multiple IPG files into + a single time series. + + :param list cumulatives: list of cumulative time series collected + over time, on for each file. Must be ordered in increasing time. + :returns: list + """ + combined_cumulatives = [] + + val_mod = 0 + for count, cumulative in enumerate(cumulatives): + # Add the previous file's maximum cumulative value + # to the current one + mod_cumulative = [val_mod + float(val) for val in cumulative] + val_mod = mod_cumulative[-1] + combined_cumulatives.extend(mod_cumulative) + + return combined_cumulatives + + def clean_ipg_data(self): + """Cleans all IPG files, or data, that was produced by an IntelPowerGadget object. + Returns a dict containing the merged data from all files. + + :returns: dict + """ + self._logger.info("Cleaning IPG data...") + + if not self._output_files: + self._logger.warning("No IPG files to clean.") + return + # If this is called a second time for the same set of files, + # then prevent it from duplicating the cleaned file entries. + if self._cleaned_files: + self._cleaned_files = [] + + # Clean each file individually and gather the results + all_results = [] + for file in self._output_files: + results, summary, output_file_path = self._clean_ipg_file(file) + self._cleaned_files.append(output_file_path) + all_results.append(results) + + # Merge all the results into a single result + combined_results = {} + for measure in all_results[0]: + lmeasure = measure.lower() + if "cumulative" not in lmeasure and "elapsed time" not in lmeasure: + # For measures which are not cumulative, or elapsed time, + # combine them without changing the data. + for count, result in enumerate(all_results): + if "system time" in lmeasure or "rdtsc" in lmeasure: + new_results = result[measure] + else: + new_results = [float(val) for val in result[measure]] + + if count == 0: + combined_results[measure] = new_results + else: + combined_results[measure].extend(new_results) + else: + # For cumulative, and elapsed time measures, we need to + # modify all values - see _combine_cumulative_rows for + # more information on this procedure. + cumulatives = [result[measure] for result in all_results] + self._logger.info( + "Combining cumulative rows for '%s' measure" % measure + ) + combined_results[measure] = self._combine_cumulative_rows(cumulatives) + + # Write merged results to a new file + merged_output_path = os.path.join( + self._output_dir_path, + "%s_merged.%s" + % (self._output_file_prefix, self._output_file_ext.replace(".", "")), + ) + + self._merged_output_path = merged_output_path + self._logger.info("Writing merged IPG results to %s" % merged_output_path) + try: + with open(merged_output_path, "w") as csvfile: + writer = csv.writer(csvfile) + writer.writerow(self._csv_header) + + # Use _csv_header list to keep the header ordering + # the same as the cleaned and raw files. + first_key = list(combined_results.keys())[0] + for row_count, _ in enumerate(combined_results[first_key]): + row = [] + for measure in self._csv_header: + row.append(combined_results[measure][row_count]) + writer.writerow(row) + except Exception as e: + self.merged_output_path = None + self._logger.warning( + "Could not write out merged results to %s due to the following error" + ", skipping this step: %s" % (merged_output_path, str(e)) + ) + + # Check that the combined results have the expected number of samples. + # pylint W16919 + expected_samples = int( + self._ipg_measure_duration / (float(self._sampling_rate) / 1000) + ) + combined_expected_samples = len(self._cleaned_files) * expected_samples + for key in combined_results: + if len(combined_results[key]) != combined_expected_samples: + self._logger.warning( + "Unexpected number of merged samples in %s for column %s - " + "expected: %s, got: %s" + % ( + merged_output_path, + key, + combined_expected_samples, + len(results[key]), + ) + ) + + self._results = combined_results + return self._results + + def format_ipg_data_to_partial_perfherder(self, duration, test_name): + """Format the merged IPG data produced by clean_ipg_data into a + partial/incomplete perfherder data blob. Returns the perfherder + data and stores it in _summarized_results. + + This perfherder data still needs more information added to it before it + can be validated against the perfherder schema. The entries returned + through here are missing many required fields. Furthermore, the 'values' + entries need to be formatted into 'subtests'. + + Here is a sample of a complete perfherder data which uses + the 'utilization' results within the subtests: + ``` + { + "framework": {"name": "raptor"}, + "type": "power", + "unit": "mWh" + "suites": [ + { + "name": "raptor-tp6-amazon-firefox-power", + "lowerIsBetter": true, + "alertThreshold": 2.0, + "subtests": [ + { + "lowerIsBetter": true, + "unit": "%", + "name": "raptor-tp6-youtube-firefox-power-cpu", + "value": 14.409090909090908, + "alertThreshold": 2.0 + }, + { + "lowerIsBetter": true, + "unit": "%", + "name": "raptor-tp6-youtube-firefox-power-gpu", + "value": 20.1, + "alertThreshold": 2.0 + }, + ] + } + ] + } + ``` + + To obtain data that is formatted to a complete perfherder data blob, + see get_complete_perfherder_data in MozPower. + + :param float duration: the actual duration of the test in case some + data needs to be cut off from the end. + :param str test_name: the name of the test. + :returns: dict + """ + self._logger.info("Formatting cleaned IPG data into partial perfherder data") + + if not self._results: + self._logger.warning( + "No merged results found - cannot format data to perfherder format." + ) + return + + def replace_measure_name(name): + """Replaces the long IPG names with shorter versions. + Returns the given name if no conversions exist. + + :param str name: name of the entry to replace. + :returns: str + """ + lname = name.lower() + if "ia " in lname: + return "processor-cores" + elif "processor " in lname: + return "processor-package" + elif "gt " in lname: + return "gpu" + elif "dram " in lname: + return "dram" + else: + return name + + # Cut out entries which surpass the test duration. + # Occurs when IPG continues past the call to stop it. + cut_results = self._results + if duration: + cutoff_index = 0 + for count, etime in enumerate(self._results["Elapsed Time (sec)"]): + if etime > duration: + cutoff_index = count + break + if cutoff_index > 0: + for measure in self._results: + cut_results[measure] = self._results[measure][:cutoff_index] + + # Get the cumulative power used in mWh + cumulative_mwh = {} + for measure in cut_results: + if "cumulative" in measure.lower() and "mwh" in measure.lower(): + cumulative_mwh[replace_measure_name(measure)] = float( + cut_results[measure][-1] + ) + + # Get the power usage rate in Watts + watt_usage = {} + for measure in cut_results: + if "watt" in measure.lower() and "limit" not in measure.lower(): + # pylint --py3k W1619 + watt_usage[replace_measure_name(measure) + "-avg"] = sum( + [float(val) for val in cut_results[measure]] + ) / len(cut_results[measure]) + watt_usage[replace_measure_name(measure) + "-max"] = max( + [float(val) for val in cut_results[measure]] + ) + + # Get average CPU and GPU utilization + average_utilization = {} + for utilization in ("CPU Utilization(%)", "GT Utilization(%)"): + if utilization not in cut_results: + self._logger.warning( + "Could not find measurements for: %s" % utilization + ) + continue + + utilized_name = utilization.lower() + if "cpu " in utilized_name: + utilized_name = "cpu" + elif "gt " in utilized_name: + utilized_name = "gpu" + + # pylint --py3k W1619 + average_utilization[utilized_name] = sum( + [float(val) for val in cut_results[utilization]] + ) / len(cut_results[utilization]) + + # Get average and maximum CPU and GPU frequency + frequency_info = {"cpu": {}, "gpu": {}} + for frequency_measure in ("CPU Frequency_0(MHz)", "GT Frequency(MHz)"): + if frequency_measure not in cut_results: + self._logger.warning( + "Could not find measurements for: %s" % frequency_measure + ) + continue + + fmeasure_name = frequency_measure.lower() + if "cpu " in fmeasure_name: + fmeasure_name = "cpu" + elif "gt " in fmeasure_name: + fmeasure_name = "gpu" + # pylint --py3k W1619 + + frequency_info[fmeasure_name]["favg"] = sum( + [float(val) for val in cut_results[frequency_measure]] + ) / len(cut_results[frequency_measure]) + + frequency_info[fmeasure_name]["fmax"] = max( + [float(val) for val in cut_results[frequency_measure]] + ) + + frequency_info[fmeasure_name]["fmin"] = min( + [float(val) for val in cut_results[frequency_measure]] + ) + + summarized_results = { + "utilization": { + "type": "power", + "test": str(test_name) + "-utilization", + "unit": "%", + "values": average_utilization, + }, + "power-usage": { + "type": "power", + "test": str(test_name) + "-cumulative", + "unit": "mWh", + "values": cumulative_mwh, + }, + "power-watts": { + "type": "power", + "test": str(test_name) + "-watts", + "unit": "W", + "values": watt_usage, + }, + "frequency-cpu": { + "type": "power", + "test": str(test_name) + "-frequency-cpu", + "unit": "MHz", + "values": frequency_info["cpu"], + }, + "frequency-gpu": { + "type": "power", + "test": str(test_name) + "-frequency-gpu", + "unit": "MHz", + "values": frequency_info["gpu"], + }, + } + + self._summarized_results = summarized_results + return self._summarized_results diff --git a/testing/mozbase/mozpower/mozpower/macintelpower.py b/testing/mozbase/mozpower/mozpower/macintelpower.py new file mode 100644 index 0000000000..18629b4a9a --- /dev/null +++ b/testing/mozbase/mozpower/mozpower/macintelpower.py @@ -0,0 +1,92 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import time + +from .intel_power_gadget import IntelPowerGadget, IPGResultsHandler +from .powerbase import PowerBase + + +class MacIntelPower(PowerBase): + """MacIntelPower is the OS and CPU dependent class for + power measurement gathering on Mac Intel-based hardware. + + :: + + from mozpower.macintelpower import MacIntelPower + + # duration and output_file_path are used in IntelPowerGadget + mip = MacIntelPower(ipg_measure_duration=600, output_file_path='power-testing') + + mip.initialize_power_measurements() + # Run test... + mip.finalize_power_measurements(test_name='raptor-test-name') + + perfherder_data = mip.get_perfherder_data() + """ + + def __init__(self, logger_name="mozpower", **kwargs): + """Initializes the MacIntelPower object. + + :param str logger_name: logging logger name. Defaults to 'mozpower'. + :param dict kwargs: optional keyword arguments passed to IntelPowerGadget. + """ + PowerBase.__init__(self, logger_name=logger_name, os="darwin", cpu="intel") + self.ipg = IntelPowerGadget(self.ipg_path, **kwargs) + self.ipg_results_handler = None + self.start_time = None + self.end_time = None + self.perfherder_data = {} + + def initialize_power_measurements(self): + """Starts power measurement gathering through IntelPowerGadget.""" + self._logger.info("Initializing power measurements...") + + # Start measuring + self.ipg.start_ipg() + + # Record start time to get an approximation of run time + self.start_time = time.time() + + def finalize_power_measurements( + self, test_name="power-testing", output_dir_path="", **kwargs + ): + """Stops power measurement gathering through IntelPowerGadget, cleans the data, + and produces partial perfherder formatted data that is stored in perfherder_data. + + :param str test_name: name of the test that was run. + :param str output_dir_path: directory to store output files. + :param dict kwargs: contains optional arguments to stop_ipg. + """ + self._logger.info("Finalizing power measurements...") + self.end_time = time.time() + + # Wait until Intel Power Gadget is done, then clean the data + # and then format it + self.ipg.stop_ipg(**kwargs) + + # Handle the results and format them to a partial perfherder format + if not output_dir_path: + output_dir_path = self.ipg.output_dir_path + + self.ipg_results_handler = IPGResultsHandler( + self.ipg.output_files, + output_dir_path, + ipg_measure_duration=self.ipg.ipg_measure_duration, + sampling_rate=self.ipg.sampling_rate, + logger_name=self.logger_name, + ) + + self.ipg_results_handler.clean_ipg_data() + self.perfherder_data = ( + self.ipg_results_handler.format_ipg_data_to_partial_perfherder( + self.end_time - self.start_time, test_name + ) + ) + + def get_perfherder_data(self): + """Returns the perfherder data output that was produced. + + :returns: dict + """ + return self.perfherder_data diff --git a/testing/mozbase/mozpower/mozpower/mozpower.py b/testing/mozbase/mozpower/mozpower/mozpower.py new file mode 100644 index 0000000000..2333fc84ee --- /dev/null +++ b/testing/mozbase/mozpower/mozpower/mozpower.py @@ -0,0 +1,376 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import os +import platform +import re +import subprocess + +import six + +from .macintelpower import MacIntelPower +from .mozpowerutils import average_summary, frequency_summary, get_logger, sum_summary + +OSCPU_COMBOS = { + "darwin-intel": MacIntelPower, +} + +SUMMARY_METHODS = { + "utilization": average_summary, + "power-usage": sum_summary, + "power-watts": frequency_summary, + "frequency-cpu": frequency_summary, + "frequency-gpu": frequency_summary, + "default": sum_summary, +} + + +class OsCpuComboMissingError(Exception): + """OsCpuComboMissingError is raised when we cannot find + a class responsible for the OS, and CPU combination that + was detected. + """ + + pass + + +class MissingProcessorInfoError(Exception): + """MissingProcessorInfoError is raised when we cannot find + the processor information on the machine. This is raised when + the file is missing (mentioned in the error message) or if + an exception occurs when we try to gather the information + from the file. + """ + + pass + + +class MozPower(object): + """MozPower provides an OS and CPU independent interface + for initializing, finalizing, and gathering power measurement + data from OS+CPU combo-dependent measurement classes. The combo + is detected automatically, and the correct class is instantiated + based on the OSCPU_COMBOS list. If it cannot be found, an + OsCpuComboMissingError will be raised. + + If a newly added power measurer does not have the required functions + `initialize_power_measurements`, `finalize_power_measurements`, + or `get_perfherder_data`, then a NotImplementedError will be + raised. + + Android power measurements are currently not supported by this + module. + + :: + + from mozpower import MozPower + + mp = MozPower(output_file_path='dir/power-testing') + + mp.initialize_power_measurements() + # Run test... + mp.finalize_power_measurements(test_name='raptor-test-name') + + perfherder_data = mp.get_perfherder_data() + """ + + def __init__( + self, + android=False, + logger_name="mozpower", + output_file_path="power-testing", + **kwargs + ): + """Initializes the MozPower object, detects OS and CPU (if not android), + and instatiates the appropriate combo-dependent class for measurements. + + :param bool android: run android power measurer. + :param str logger_name: logging logger name. Defaults to 'mozpower'. + :param str output_file_path: path to where output files will be stored. + Can include a prefix for the output files, i.e. 'dir/raptor-test' + would output data to 'dir' with the prefix 'raptor-test'. + Defaults to 'power-testing', the current directory using + the prefix 'power-testing'. + :param dict kwargs: optional keyword arguments passed to power measurer. + :raises: * OsCpuComboMissingError + * NotImplementedError + """ + self.measurer = None + self._os = None + self._cpu = None + self._logger = get_logger(logger_name) + + if android: + self._logger.error("Android power usage measurer has not been implemented") + raise NotImplementedError + else: + self._os = self._get_os().lower() + cpu = six.text_type(self._get_processor_info().lower()) + + if "intel" in cpu: + self._cpu = "intel" + else: + self._cpu = "arm64" + + # OS+CPU combos are specified through strings such as 'darwin-intel' + # for mac power measurement on intel-based machines. If none exist in + # OSCPU_COMBOS, OsCpuComboMissingError will be raised. + measurer = None + oscpu_combo = "%s-%s" % (self._os, self._cpu) + if oscpu_combo in OSCPU_COMBOS: + measurer = OSCPU_COMBOS[oscpu_combo] + else: + raise OsCpuComboMissingError( + "Cannot find OS+CPU combo for %s" % oscpu_combo + ) + + if measurer: + self._logger.info( + "Intializing measurer %s for %s power measurements, see below for errors..." + % (measurer.__name__, oscpu_combo) + ) + self.measurer = measurer( + logger_name=logger_name, output_file_path=output_file_path, **kwargs + ) + + def _get_os(self): + """Returns the operating system of the machine being tested. platform.system() + returns 'darwin' on MacOS, 'windows' on Windows, and 'linux' on Linux systems. + + :returns: str + """ + return platform.system() + + def _get_processor_info(self): + """Returns the processor model type of the machine being tested. + Each OS has it's own way of storing this information. Raises + MissingProcessorInfoError if we cannot get the processor info + from the expected locations. + + :returns: str + :raises: * MissingProcessorInfoError + """ + model = "" + + if self._get_os() == "Windows": + model = platform.processor() + + elif self._get_os() == "Darwin": + proc_info_path = "/usr/sbin/sysctl" + command = [proc_info_path, "-n", "machdep.cpu.brand_string"] + + if not os.path.exists(proc_info_path): + raise MissingProcessorInfoError( + "Missing processor info file for darwin platform, " + "expecting it here %s" % proc_info_path + ) + + try: + model = subprocess.check_output(command).strip() + except subprocess.CalledProcessError as e: + error_log = str(e) + if e.output: + error_log = e.output.decode() + raise MissingProcessorInfoError( + "Error while attempting to get darwin processor information " + "from %s (exists) with the command %s: %s" + % (proc_info_path, str(command), error_log) + ) + + elif self._get_os() == "Linux": + proc_info_path = "/proc/cpuinfo" + model_re = re.compile(r""".*model name\s+[:]\s+(.*)\s+""") + + if not os.path.exists(proc_info_path): + raise MissingProcessorInfoError( + "Missing processor info file for linux platform, " + "expecting it here %s" % proc_info_path + ) + + try: + with open(proc_info_path) as cpuinfo: + for line in cpuinfo: + if not line.strip(): + continue + match = model_re.match(line) + if match: + model = match.group(1) + if not model: + raise Exception( + "No 'model name' entries found in the processor info file" + ) + except Exception as e: + raise MissingProcessorInfoError( + "Error while attempting to get linux processor information " + "from %s (exists): %s" % (proc_info_path, str(e)) + ) + + return model + + def initialize_power_measurements(self, **kwargs): + """Starts the power measurements by calling the power measurer's + `initialize_power_measurements` function. + + :param dict kwargs: keyword arguments for power measurer initialization + function if they are needed. + """ + if self.measurer is None: + return + self.measurer.initialize_power_measurements(**kwargs) + + def finalize_power_measurements(self, **kwargs): + """Stops the power measurements by calling the power measurer's + `finalize_power_measurements` function. + + :param dict kwargs: keyword arguments for power measurer finalization + function if they are needed. + """ + if self.measurer is None: + return + self.measurer.finalize_power_measurements(**kwargs) + + def get_perfherder_data(self): + """Returns the partial perfherder data output produced by the measurer. + For a complete perfherder data blob, see get_full_perfherder_data. + + :returns: dict + """ + if self.measurer is None: + return + return self.measurer.get_perfherder_data() + + def _summarize_values(self, datatype, values): + """Summarizes the given values based on the type of the + data. See SUMMARY_METHODS for the methods used for each + known data type. Defaults to using the sum of the values + when a data type cannot be found. + + Data type entries in SUMMARY_METHODS are case-sensitive. + + :param str datastype: the measurement type being summarized. + :param list values: the values to summarize. + :returns: float + """ + if datatype not in SUMMARY_METHODS: + self._logger.warning( + "Missing summary method for data type %s, defaulting to sum" % datatype + ) + datatype = "default" + + summary_func = SUMMARY_METHODS[datatype] + return summary_func(values) + + def get_full_perfherder_data( + self, framework, lowerisbetter=True, alertthreshold=2.0 + ): + """Returns a list of complete perfherder data blobs compiled from the + partial perfherder data blob returned from the measurer. Each key entry + (measurement type) in the partial perfherder data is parsed into its + own suite within a single perfherder data blob. + + For example, a partial perfherder data blob such as: + + :: + + { + 'utilization': {<perfherder_data>}, + 'power-usage': {<perfherder_data>} + } + + would produce two suites within a single perfherder data blobs - + one for utilization, and one for power-usage. + + Note that the 'values' entry must exist, otherwise the measurement + type is skipped. Furthermore, if 'name', 'unit', or 'type' is missing + we default to: + + :: + + { + 'name': 'mozpower', + 'unit': 'mWh', + 'type': 'power' + } + + Subtests produced for each sub-suite (measurement type), have the naming + pattern: <measurement_type>-<measured_name> + + Utilization of cpu would have the following name: 'utilization-cpu' + Power-usage for cpu has the following name: 'power-usage-cpu' + + :param str framework: name of the framework being tested, i.e. 'raptor'. + :param bool lowerisbetter: if set to true, low values are better than high ones. + :param float alertthreshold: determines the crossing threshold at + which an alert is generated. + :returns: dict + """ + if self.measurer is None: + return + + # Get the partial data, and the measurers name for + # logging purposes. + partial_perfherder_data = self.get_perfherder_data() + measurer_name = self.measurer.__class__.__name__ + + suites = [] + perfherder_data = {"framework": {"name": framework}, "suites": suites} + + for measurement_type in partial_perfherder_data: + self._logger.info("Summarizing %s data" % measurement_type) + dataset = partial_perfherder_data[measurement_type] + + # Skip this measurement type if the 'values' entry + # doesn't exist, and output a warning. + if "values" not in dataset: + self._logger.warning( + "Missing 'values' entry in partial perfherder data for measurement type %s " + "obtained from %s. This measurement type will not be processed." + % (measurement_type, measurer_name) + ) + continue + + # Get the settings, if they exist, otherwise output + # a warning and use a default entry. + settings = {"test": "mozpower", "unit": "mWh", "type": "power"} + + for setting in settings: + if setting in dataset: + settings[setting] = dataset[setting] + else: + self._logger.warning( + "Missing '%s' entry in partial perfherder data for measurement type %s " + "obtained from %s, using %s as the default" + % (setting, measurement_type, measurer_name, settings[setting]) + ) + + subtests = [] + suite = { + "name": "%s-%s" % (settings["test"], measurement_type), + "type": settings["type"], + "value": 0, + "subtests": subtests, + "lowerIsBetter": lowerisbetter, + "unit": settings["unit"], + "alertThreshold": alertthreshold, + } + + # Parse the 'values' entries into subtests + values = [] + for measure in dataset["values"]: + value = dataset["values"][measure] + subtest = { + "name": "%s-%s" % (measurement_type, measure), + "value": float(value), + "lowerIsBetter": lowerisbetter, + "alertThreshold": alertthreshold, + "unit": settings["unit"], + } + values.append((value, measure)) + subtests.append(subtest) + + # Summarize the data based on the measurement type + if len(values) > 0: + suite["value"] = self._summarize_values(measurement_type, values) + suites.append(suite) + + return perfherder_data diff --git a/testing/mozbase/mozpower/mozpower/mozpowerutils.py b/testing/mozbase/mozpower/mozpower/mozpowerutils.py new file mode 100644 index 0000000000..bedf272a0e --- /dev/null +++ b/testing/mozbase/mozpower/mozpower/mozpowerutils.py @@ -0,0 +1,58 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +def get_logger(logger_name): + """Returns the logger that should be used based on logger_name. + Defaults to the logging logger if mozlog cannot be imported. + + :returns: mozlog or logging logger object + """ + logger = None + try: + import mozlog + + logger = mozlog.get_default_logger(logger_name) + except ImportError: + pass + + if logger is None: + import logging + + logging.basicConfig() + logger = logging.getLogger(logger_name) + return logger + + +def average_summary(values): + """Averages all given values. + + :param list values: list of values to average. + :returns: float + """ + # pylint --py3k W1619 + return sum([float(v[0]) for v in values]) / len(values) + + +def sum_summary(values): + """Adds all values together. + + :param list values: list of values to sum. + :returns: float + """ + return sum([float(v[0]) for v in values]) + + +def frequency_summary(values): + """Returns the average frequency as the summary value. + + :param list values: list of values to search in. + :returns: float + """ + avgfreq = 0 + for val, name in values: + if "avg" in name: + avgfreq = float(val) + break + return avgfreq diff --git a/testing/mozbase/mozpower/mozpower/powerbase.py b/testing/mozbase/mozpower/mozpower/powerbase.py new file mode 100644 index 0000000000..3eb91e3e3b --- /dev/null +++ b/testing/mozbase/mozpower/mozpower/powerbase.py @@ -0,0 +1,122 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import os + +from .mozpowerutils import get_logger + + +class IPGExecutableMissingError(Exception): + """IPGExecutableMissingError is raised when we cannot find + the executable for Intel Power Gadget at the expected location. + """ + + pass + + +class PlatformUnsupportedError(Exception): + """PlatformUnsupportedError is raised when we cannot find + an expected IPG path for the OS being tested. + """ + + pass + + +class PowerBase(object): + """PowerBase provides an interface for power measurement objects + that depend on the os and cpu. When using this class as a base class + the `initialize_power_measurements`, `finalize_power_measurements`, + and `get_perfherder_data` functions must be implemented, otherwise + a NotImplementedError will be raised. + + PowerBase should only be used as the base class for other + classes and should not be instantiated directly. To enforce this + restriction calling PowerBase's constructor will raise a + NonImplementedError exception. + + :: + + from mozpower.powerbase import PowerBase + + try: + pb = PowerBase() + except NotImplementedError: + print "PowerBase cannot be instantiated." + + """ + + def __init__(self, logger_name="mozpower", os=None, cpu=None): + """Initializes the PowerBase object. + + :param str logger_name: logging logger name. Defaults to 'mozpower'. + :param str os: operating system being tested. Defaults to None. + :param str cpu: cpu type being tested (either intel or arm64). Defaults to None. + :raises: * NotImplementedError + """ + if self.__class__ == PowerBase: + raise NotImplementedError + + self._logger_name = logger_name + self._logger = get_logger(logger_name) + self._os = os + self._cpu = cpu + self._ipg_path = self.get_ipg_path() + + @property + def ipg_path(self): + return self._ipg_path + + @property + def logger_name(self): + return self._logger_name + + def initialize_power_measurements(self): + """Starts power measurement gathering, must be implemented by subclass. + + :raises: * NotImplementedError + """ + raise NotImplementedError + + def finalize_power_measurements(self, test_name="power-testing", **kwargs): + """Stops power measurement gathering, must be implemented by subclass. + + :raises: * NotImplementedError + """ + raise NotImplementedError + + def get_perfherder_data(self): + """Returns the perfherder data output produced from the tests, must + be implemented by subclass. + + :raises: * NotImplementedError + """ + raise NotImplementedError + + def get_ipg_path(self): + """Returns the path to where we expect to find Intel Power Gadget + depending on the OS being tested. Raises a PlatformUnsupportedError + if the OS being tested is unsupported, and raises a IPGExecutableMissingError + if the Intel Power Gadget executable cannot be found at the expected + location. + + :returns: str + :raises: * IPGExecutableMissingError + * PlatformUnsupportedError + """ + if self._cpu != "intel": + return None + + if self._os == "darwin": + exe_path = "/Applications/Intel Power Gadget/PowerLog" + else: + raise PlatformUnsupportedError( + "%s platform currently not supported for Intel Power Gadget measurements" + % self._os + ) + if not os.path.exists(exe_path): + raise IPGExecutableMissingError( + "Intel Power Gadget is not installed, or cannot be found at the location %s" + % exe_path + ) + + return exe_path diff --git a/testing/mozbase/mozpower/setup.cfg b/testing/mozbase/mozpower/setup.cfg new file mode 100644 index 0000000000..2a9acf13da --- /dev/null +++ b/testing/mozbase/mozpower/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/testing/mozbase/mozpower/setup.py b/testing/mozbase/mozpower/setup.py new file mode 100644 index 0000000000..4d1cb0cb00 --- /dev/null +++ b/testing/mozbase/mozpower/setup.py @@ -0,0 +1,34 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from setuptools import setup + +PACKAGE_NAME = "mozpower" +PACKAGE_VERSION = "1.1.2" + +deps = ["mozlog >= 6.0", "mozdevice >= 4.0.0,<5"] + +setup( + name=PACKAGE_NAME, + version=PACKAGE_VERSION, + description="Mozilla-authored power usage measurement tools", + long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html", + classifiers=[ + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + ], + # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers + keywords="", + author="Mozilla Performance Test Engineering Team", + author_email="tools@lists.mozilla.org", + url="https://wiki.mozilla.org/Auto-tools/Projects/Mozbase", + license="MPL", + packages=["mozpower"], + include_package_data=True, + zip_safe=False, + install_requires=deps, + entry_points=""" + # -*- Entry points: -*- + """, +) diff --git a/testing/mozbase/mozpower/tests/conftest.py b/testing/mozbase/mozpower/tests/conftest.py new file mode 100644 index 0000000000..9d81b0229b --- /dev/null +++ b/testing/mozbase/mozpower/tests/conftest.py @@ -0,0 +1,103 @@ +import os +import tempfile +import time +from unittest import mock + +import pytest +from mozpower import MozPower +from mozpower.intel_power_gadget import IntelPowerGadget, IPGResultsHandler +from mozpower.macintelpower import MacIntelPower +from mozpower.powerbase import PowerBase + + +def os_side_effect(*args, **kwargs): + """Used as a side effect to os.path.exists when + checking if the Intel Power Gadget executable exists. + """ + return True + + +def subprocess_side_effect(*args, **kwargs): + """Used as a side effect when running the Intel Power + Gadget tool. + """ + time.sleep(1) + + +@pytest.fixture(scope="function") +def powermeasurer(): + """Returns a testing subclass of the PowerBase class + for testing. + """ + + class PowerMeasurer(PowerBase): + pass + + return PowerMeasurer() + + +@pytest.fixture(scope="function") +def ipg_obj(): + """Returns an IntelPowerGadget object with the test + output file path. + """ + return IntelPowerGadget( + "ipg-path", + output_file_path=os.path.abspath(os.path.dirname(__file__)) + + "/files/raptor-tp6-amazon-firefox_powerlog", + ) + + +@pytest.fixture(scope="function") +def ipg_rh_obj(): + """Returns an IPGResultsHandler object set up with the + test files and cleans up the directory after the tests + are complete. + """ + base_path = os.path.abspath(os.path.dirname(__file__)) + "/files/" + tmpdir = tempfile.mkdtemp() + + # Return the results handler for the test + yield IPGResultsHandler( + [ + base_path + "raptor-tp6-amazon-firefox_powerlog_1_.txt", + base_path + "raptor-tp6-amazon-firefox_powerlog_2_.txt", + base_path + "raptor-tp6-amazon-firefox_powerlog_3_.txt", + ], + tmpdir, + ) + + +@pytest.fixture(scope="function") +def macintelpower_obj(): + """Returns a MacIntelPower object with subprocess.check_output + and os.path.exists calls patched with side effects. + """ + with mock.patch("subprocess.check_output") as subprocess_mock: + with mock.patch("os.path.exists") as os_mock: + subprocess_mock.side_effect = subprocess_side_effect + os_mock.side_effect = os_side_effect + + yield MacIntelPower(ipg_measure_duration=2) + + +@pytest.fixture(scope="function") +def mozpower_obj(): + """Returns a MozPower object with subprocess.check_output + and os.path.exists calls patched with side effects. + """ + with mock.patch.object( + MozPower, "_get_os", return_value="Darwin" + ) as _, mock.patch.object( + MozPower, "_get_processor_info", return_value="GenuineIntel" + ) as _, mock.patch.object( + MacIntelPower, "get_ipg_path", return_value="/" + ) as _, mock.patch( + "subprocess.check_output" + ) as subprocess_mock, mock.patch( + "os.path.exists" + ) as os_mock: + subprocess_mock.side_effect = subprocess_side_effect + os_mock.side_effect = os_side_effect + + yield MozPower(ipg_measure_duration=2) diff --git a/testing/mozbase/mozpower/tests/files/emptyfile.txt b/testing/mozbase/mozpower/tests/files/emptyfile.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mozbase/mozpower/tests/files/emptyfile.txt diff --git a/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_1_.txt b/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_1_.txt new file mode 100644 index 0000000000..d5cc3f91a5 --- /dev/null +++ b/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_1_.txt @@ -0,0 +1,30 @@ +"System Time","RDTSC","Elapsed Time (sec)","CPU Utilization(%)","CPU Frequency_0(MHz)","Processor Power_0(Watt)","Cumulative Processor Energy_0(Joules)","Cumulative Processor Energy_0(mWh)","IA Power_0(Watt)","Cumulative IA Energy_0(Joules)","Cumulative IA Energy_0(mWh)","Package Temperature_0(C)","Package Hot_0","DRAM Power_0(Watt)","Cumulative DRAM Energy_0(Joules)","Cumulative DRAM Energy_0(mWh)","GT Power_0(Watt)","Cumulative GT Energy_0(Joules)","Cumulative GT Energy_0(mWh)","Package Power Limit_0(Watt)","GT Frequency(MHz)" +"12:11:05:769","61075218263548"," 2.002"," 14.000"," 3400"," 23.427"," 23.460"," 6.517"," 21.019"," 21.048"," 5.847"," 75","0"," 0.877"," 0.878"," 0.244"," 0.020"," 0.020"," 0.006"," 45.000"," 0" +"12:11:06:774","61077822279584"," 3.007"," 20.000"," 3000"," 25.014"," 48.590"," 13.497"," 22.386"," 43.538"," 12.094"," 74","0"," 1.194"," 2.078"," 0.577"," 0.029"," 0.049"," 0.014"," 45.000"," 0" +"12:11:07:778","61080424421708"," 4.011"," 8.000"," 1300"," 9.512"," 58.140"," 16.150"," 6.904"," 50.469"," 14.019"," 65","0"," 0.836"," 2.917"," 0.810"," 0.007"," 0.057"," 0.016"," 45.000"," 0" +"12:11:08:781","61083023535972"," 5.013"," 1.000"," 1300"," 1.786"," 59.931"," 16.647"," 0.687"," 51.158"," 14.210"," 63","0"," 0.585"," 3.504"," 0.973"," 0.000"," 0.057"," 0.016"," 45.000"," 0" +"12:11:09:784","61085623402302"," 6.016"," 4.000"," 4000"," 5.249"," 65.195"," 18.110"," 3.743"," 54.912"," 15.253"," 75","0"," 0.660"," 4.166"," 1.157"," 0.003"," 0.059"," 0.017"," 45.000"," 0" +"12:11:10:787","61088224087008"," 7.020"," 20.000"," 3000"," 35.118"," 100.432"," 27.898"," 31.647"," 86.666"," 24.074"," 72","0"," 1.048"," 5.218"," 1.449"," 0.016"," 0.076"," 0.021"," 45.000"," 0" +"12:11:11:791","61090825821126"," 8.024"," 13.000"," 3000"," 25.436"," 125.965"," 34.990"," 22.228"," 108.979"," 30.272"," 71","0"," 0.868"," 6.089"," 1.691"," 0.004"," 0.080"," 0.022"," 45.000"," 0" +"12:11:12:795","61093429020836"," 9.028"," 5.000"," 1300"," 5.266"," 131.253"," 36.459"," 3.270"," 112.263"," 31.184"," 64","0"," 0.711"," 6.802"," 1.890"," 0.000"," 0.080"," 0.022"," 45.000"," 0" +"12:11:13:796","61096024160928"," 10.029"," 5.000"," 4000"," 11.070"," 142.338"," 39.538"," 9.613"," 121.888"," 33.858"," 78","0"," 0.650"," 7.453"," 2.070"," 0.000"," 0.080"," 0.022"," 45.000"," 0" +"12:11:13:796","61096024279264"," 10.029"," 57.000"," 4000"," 0.000"," 142.338"," 39.538"," 0.000"," 121.888"," 33.858"," 78","0"," 0.000"," 7.453"," 2.070"," 0.000"," 0.080"," 0.022"," 45.000"," 0" +
+"Total Elapsed Time (sec) = 10.029232" +"Measured RDTSC Frequency (GHz) = 2.592" +
+"Cumulative Processor Energy_0 (Joules) = 142.337524" +"Cumulative Processor Energy_0 (mWh) = 39.538201" +"Average Processor Power_0 (Watt) = 14.192265" +
+"Cumulative IA Energy_0 (Joules) = 121.888000" +"Cumulative IA Energy_0 (mWh) = 33.857778" +"Average IA Power_0 (Watt) = 12.153273" +
+"Cumulative DRAM Energy_0 (Joules) = 7.453308" +"Cumulative DRAM Energy_0 (mWh) = 2.070363" +"Average DRAM Power_0 (Watt) = 0.743158" +
+"Cumulative GT Energy_0 (Joules) = 0.079834" +"Cumulative GT Energy_0 (mWh) = 0.022176" +"Average GT Power_0 (Watt) = 0.007960" diff --git a/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_2_.txt b/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_2_.txt new file mode 100644 index 0000000000..9157ad1fad --- /dev/null +++ b/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_2_.txt @@ -0,0 +1,30 @@ +"System Time","RDTSC","Elapsed Time (sec)","CPU Utilization(%)","CPU Frequency_0(MHz)","Processor Power_0(Watt)","Cumulative Processor Energy_0(Joules)","Cumulative Processor Energy_0(mWh)","IA Power_0(Watt)","Cumulative IA Energy_0(Joules)","Cumulative IA Energy_0(mWh)","Package Temperature_0(C)","Package Hot_0","DRAM Power_0(Watt)","Cumulative DRAM Energy_0(Joules)","Cumulative DRAM Energy_0(mWh)","GT Power_0(Watt)","Cumulative GT Energy_0(Joules)","Cumulative GT Energy_0(mWh)","Package Power Limit_0(Watt)","GT Frequency(MHz)" +"12:11:15:821","61101270635674"," 2.007"," 9.000"," 4200"," 10.341"," 10.382"," 2.884"," 7.710"," 7.740"," 2.150"," 80","0"," 0.828"," 0.831"," 0.231"," 0.000"," 0.000"," 0.000"," 45.000"," 0" +"12:11:16:823","61103869919264"," 3.010"," 4.000"," 1300"," 9.353"," 19.762"," 5.489"," 8.079"," 15.842"," 4.400"," 64","0"," 0.597"," 1.430"," 0.397"," 0.000"," 0.000"," 0.000"," 45.000"," 0" +"12:11:17:826","61106469262970"," 4.013"," 19.000"," 2500"," 21.127"," 40.950"," 11.375"," 17.753"," 33.645"," 9.346"," 69","0"," 1.091"," 2.524"," 0.701"," 0.011"," 0.011"," 0.003"," 45.000"," 0" +"12:11:18:827","61109063770516"," 5.014"," 11.000"," 2000"," 11.620"," 52.581"," 14.606"," 8.333"," 41.986"," 11.663"," 64","0"," 0.961"," 3.486"," 0.968"," 0.000"," 0.011"," 0.003"," 45.000"," 0" +"12:11:19:831","61111666134444"," 6.018"," 3.000"," 1300"," 2.722"," 55.314"," 15.365"," 1.497"," 43.489"," 12.080"," 64","0"," 0.620"," 4.108"," 1.141"," 0.000"," 0.011"," 0.003"," 45.000"," 0" +"12:11:20:835","61114266619236"," 7.021"," 14.000"," 3400"," 17.108"," 72.478"," 20.133"," 14.538"," 58.075"," 16.132"," 79","0"," 0.918"," 5.029"," 1.397"," 0.004"," 0.016"," 0.004"," 45.000"," 0" +"12:11:21:835","61116859007218"," 8.021"," 15.000"," 3000"," 16.651"," 89.132"," 24.759"," 13.055"," 71.132"," 19.759"," 70","0"," 1.103"," 6.132"," 1.703"," 0.005"," 0.020"," 0.006"," 45.000"," 0" +"12:11:22:836","61119453982614"," 9.023"," 4.000"," 2000"," 3.334"," 92.470"," 25.686"," 1.974"," 73.109"," 20.308"," 64","0"," 0.654"," 6.787"," 1.885"," 0.000"," 0.020"," 0.006"," 45.000"," 0" +"12:11:23:840","61122057636318"," 10.027"," 12.000"," 3400"," 13.925"," 106.458"," 29.572"," 11.603"," 84.764"," 23.546"," 79","0"," 0.856"," 7.647"," 2.124"," 0.006"," 0.027"," 0.007"," 45.000"," 0" +"12:11:23:840","61122057733984"," 10.027"," 66.000"," 3400"," 0.000"," 106.458"," 29.572"," 0.000"," 84.764"," 23.546"," 79","0"," 0.000"," 7.647"," 2.124"," 0.000"," 0.027"," 0.007"," 45.000"," 0" +
+"Total Elapsed Time (sec) = 10.027134" +"Measured RDTSC Frequency (GHz) = 2.592" +
+"Cumulative Processor Energy_0 (Joules) = 106.457581" +"Cumulative Processor Energy_0 (mWh) = 29.571550" +"Average Processor Power_0 (Watt) = 10.616950" +
+"Cumulative IA Energy_0 (Joules) = 84.764404" +"Cumulative IA Energy_0 (mWh) = 23.545668" +"Average IA Power_0 (Watt) = 8.453503" +
+"Cumulative DRAM Energy_0 (Joules) = 7.647095" +"Cumulative DRAM Energy_0 (mWh) = 2.124193" +"Average DRAM Power_0 (Watt) = 0.762640" +
+"Cumulative GT Energy_0 (Joules) = 0.026550" +"Cumulative GT Energy_0 (mWh) = 0.007375" +"Average GT Power_0 (Watt) = 0.002648" diff --git a/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_3_.txt b/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_3_.txt new file mode 100644 index 0000000000..a2779d93a1 --- /dev/null +++ b/testing/mozbase/mozpower/tests/files/raptor-tp6-amazon-firefox_powerlog_3_.txt @@ -0,0 +1,30 @@ +"System Time","RDTSC","Elapsed Time (sec)","CPU Utilization(%)","CPU Frequency_0(MHz)","Processor Power_0(Watt)","Cumulative Processor Energy_0(Joules)","Cumulative Processor Energy_0(mWh)","IA Power_0(Watt)","Cumulative IA Energy_0(Joules)","Cumulative IA Energy_0(mWh)","Package Temperature_0(C)","Package Hot_0","DRAM Power_0(Watt)","Cumulative DRAM Energy_0(Joules)","Cumulative DRAM Energy_0(mWh)","GT Power_0(Watt)","Cumulative GT Energy_0(Joules)","Cumulative GT Energy_0(mWh)","Package Power Limit_0(Watt)","GT Frequency(MHz)" +"12:11:25:867","61127310813944"," 2.006"," 6.000"," 2000"," 4.862"," 4.879"," 1.355"," 3.087"," 3.097"," 0.860"," 65","0"," 0.774"," 0.776"," 0.216"," 0.021"," 0.022"," 0.006"," 45.000"," 0" +"12:11:26:867","61129903677124"," 3.006"," 2.000"," 1300"," 2.883"," 7.763"," 2.156"," 1.253"," 4.350"," 1.208"," 63","0"," 0.644"," 1.421"," 0.395"," 0.018"," 0.039"," 0.011"," 45.000"," 0" +"12:11:27:873","61132509307964"," 4.011"," 2.000"," 1300"," 1.788"," 9.561"," 2.656"," 0.699"," 5.054"," 1.404"," 63","0"," 0.538"," 1.961"," 0.545"," 0.000"," 0.039"," 0.011"," 45.000"," 0" +"12:11:28:877","61135111021262"," 5.015"," 0.000"," 1300"," 1.177"," 10.743"," 2.984"," 0.321"," 5.375"," 1.493"," 63","0"," 0.535"," 2.499"," 0.694"," 0.000"," 0.039"," 0.011"," 45.000"," 0" +"12:11:29:881","61137714179976"," 6.020"," 0.000"," 1300"," 1.064"," 11.811"," 3.281"," 0.251"," 5.627"," 1.563"," 61","0"," 0.520"," 3.021"," 0.839"," 0.000"," 0.039"," 0.011"," 45.000"," 0" +"12:11:30:881","61140307250904"," 7.020"," 0.000"," 1300"," 1.053"," 12.864"," 3.573"," 0.256"," 5.884"," 1.634"," 61","0"," 0.528"," 3.550"," 0.986"," 0.000"," 0.039"," 0.011"," 45.000"," 0" +"12:11:31:885","61142907976768"," 8.023"," 0.000"," 1300"," 1.069"," 13.937"," 3.871"," 0.274"," 6.159"," 1.711"," 61","0"," 0.524"," 4.075"," 1.132"," 0.000"," 0.039"," 0.011"," 45.000"," 0" +"12:11:32:886","61145503940512"," 9.025"," 0.000"," 1300"," 1.008"," 14.947"," 4.152"," 0.228"," 6.387"," 1.774"," 60","0"," 0.522"," 4.598"," 1.277"," 0.000"," 0.039"," 0.011"," 45.000"," 0" +"12:11:33:891","61148109378824"," 10.030"," 0.000"," 1300"," 1.007"," 15.959"," 4.433"," 0.230"," 6.618"," 1.838"," 61","0"," 0.522"," 5.123"," 1.423"," 0.000"," 0.039"," 0.011"," 45.000"," 0" +"12:11:33:892","61148109766160"," 10.030"," 27.000"," 1300"," 0.000"," 15.959"," 4.433"," 0.000"," 6.618"," 1.838"," 61","0"," 2.448"," 5.123"," 1.423"," 0.000"," 0.039"," 0.011"," 45.000"," 0" +
+"Total Elapsed Time (sec) = 10.030310" +"Measured RDTSC Frequency (GHz) = 2.592" +
+"Cumulative Processor Energy_0 (Joules) = 15.958862" +"Cumulative Processor Energy_0 (mWh) = 4.433017" +"Average Processor Power_0 (Watt) = 1.591064" +
+"Cumulative IA Energy_0 (Joules) = 6.618042" +"Cumulative IA Energy_0 (mWh) = 1.838345" +"Average IA Power_0 (Watt) = 0.659804" +
+"Cumulative DRAM Energy_0 (Joules) = 5.122925" +"Cumulative DRAM Energy_0 (mWh) = 1.423035" +"Average DRAM Power_0 (Watt) = 0.510744" +
+"Cumulative GT Energy_0 (Joules) = 0.039490" +"Cumulative GT Energy_0 (mWh) = 0.010969" +"Average GT Power_0 (Watt) = 0.003937" diff --git a/testing/mozbase/mozpower/tests/files/valueerrorfile.txt b/testing/mozbase/mozpower/tests/files/valueerrorfile.txt new file mode 100644 index 0000000000..8f2d3485eb --- /dev/null +++ b/testing/mozbase/mozpower/tests/files/valueerrorfile.txt @@ -0,0 +1,30 @@ +"System Time","RDTSC","Elapsed Time (sec)","CPU Utilization(%)","CPU Frequency_0(MHz)","Processor Power_0(Watt)","Cumulative Processor Energy_0(Joules)","Cumulative Processor Energy_0(mWh)","IA Power_0(Watt)","Cumulative IA Energy_0(Joules)","Cumulative IA Energy_0(mWh)","Package Temperature_0(C)","Package Hot_0","DRAM Power_0(Watt)","Cumulative DRAM Energy_0(Joules)","Cumulative DRAM Energy_0(mWh)","GT Power_0(Watt)","Cumulative GT Energy_0(Joules)","Cumulative GT Energy_0(mWh)","Package Power Limit_0(Watt)","GT Frequency(MHz)" +"12:11:05:769","61075218263548"," 2.002"," 14.000"," 3400"," 23.427"," 23.460"," 6.517"," 21.019"," 21.048"," 5.847"," 75","0"," 0.877"," 0.878"," 0.244"," 0.020"," 0.020"," 0.006"," 45.000"," 0" +"12:11:06:774","61077822279584"," 3.007"," 20.000"," 3000"," 25.014"," 48.590"," 13.497"," 22.386"," 43.538"," 12.094"," 74","0"," 1.194"," 2.078"," 0.577"," 0.029"," 0.049"," 0.014"," 45.000"," 0" +"12:11:07:778","61080424421708"," 4.011"," 8.000"," 1300"," 9.512"," 58.140"," 16.150"," 6.904"," 50.469"," 14.019"," 65","0"," 0.836"," 2.917"," 0.810"," 0.007"," 0.057"," 0.016"," 45.000"," 0" +"12:11:08:781","61083023535972"," 5.013"," 1.000"," 1300"," 1.786"," 59.931"," 16.647"," 0.687"," 51.158"," 14.210"," 63","0"," 0.585"," 3.504"," 0.973"," 0.000"," 0.057"," 0.016"," 45.000"," 0" +"12:11:09:784","61085623402302"," none"," 4.000"," 4000"," 5.249"," 65.195"," 18.110"," 3.743"," 54.912"," 15.253"," 75","0"," 0.660"," 4.166"," 1.157"," 0.003"," 0.059"," 0.017"," 45.000"," 0" +"12:11:10:787","61088224087008"," 7.020"," 20.000"," 3000"," 35.118"," 100.432"," 27.898"," 31.647"," 86.666"," 24.074"," 72","0"," 1.048"," 5.218"," 1.449"," 0.016"," 0.076"," 0.021"," 45.000"," 0" +"12:11:11:791","61090825821126"," 8.024"," 13.000"," 3000"," 25.436"," 125.965"," 34.990"," 22.228"," 108.979"," 30.272"," 71","0"," 0.868"," 6.089"," 1.691"," 0.004"," 0.080"," 0.022"," 45.000"," 0" +"12:11:12:795","61093429020836"," 9.028"," 5.000"," 1300"," 5.266"," 131.253"," 36.459"," 3.270"," 112.263"," 31.184"," 64","0"," 0.711"," 6.802"," 1.890"," 0.000"," 0.080"," 0.022"," 45.000"," 0" +"12:11:13:796","61096024160928"," 10.029"," 5.000"," 4000"," 11.070"," 142.338"," 39.538"," 9.613"," 121.888"," 33.858"," 78","0"," 0.650"," 7.453"," 2.070"," 0.000"," 0.080"," 0.022"," 45.000"," 0" +"12:11:13:796","61096024279264"," 10.029"," 57.000"," 4000"," 0.000"," 142.338"," 39.538"," 0.000"," 121.888"," 33.858"," 78","0"," 0.000"," 7.453"," 2.070"," 0.000"," 0.080"," 0.022"," 45.000"," 0" + +"Total Elapsed Time (sec) = 10.029232" +"Measured RDTSC Frequency (GHz) = 2.592" + +"Cumulative Processor Energy_0 (Joules) = 142.337524" +"Cumulative Processor Energy_0 (mWh) = 39.538201" +"Average Processor Power_0 (Watt) = 14.192265" + +"Cumulative IA Energy_0 (Joules) = 121.888000" +"Cumulative IA Energy_0 (mWh) = 33.857778" +"Average IA Power_0 (Watt) = 12.153273" + +"Cumulative DRAM Energy_0 (Joules) = 7.453308" +"Cumulative DRAM Energy_0 (mWh) = 2.070363" +"Average DRAM Power_0 (Watt) = 0.743158" + +"Cumulative GT Energy_0 (Joules) = 0.079834" +"Cumulative GT Energy_0 (mWh) = 0.022176" +"Average GT Power_0 (Watt) = 0.007960" diff --git a/testing/mozbase/mozpower/tests/manifest.ini b/testing/mozbase/mozpower/tests/manifest.ini new file mode 100644 index 0000000000..2e825e2fa5 --- /dev/null +++ b/testing/mozbase/mozpower/tests/manifest.ini @@ -0,0 +1,6 @@ +[DEFAULT] +subsuite = mozbase +[test_powerbase.py] +[test_intelpowergadget.py] +[test_macintelpower.py] +[test_mozpower.py] diff --git a/testing/mozbase/mozpower/tests/test_intelpowergadget.py b/testing/mozbase/mozpower/tests/test_intelpowergadget.py new file mode 100644 index 0000000000..9be01cb720 --- /dev/null +++ b/testing/mozbase/mozpower/tests/test_intelpowergadget.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python + +import datetime +import os +import time +from unittest import mock + +import mozunit +import pytest +import six +from mozpower.intel_power_gadget import ( + IPGEmptyFileError, + IPGMissingOutputFileError, + IPGTimeoutError, + IPGUnknownValueTypeError, +) + + +def thread_is_alive(thread): + if six.PY2: + return thread.isAlive() + return thread.is_alive() + + +def test_ipg_pathsplitting(ipg_obj): + """Tests that the output file path and prefix was properly split. + This test assumes that it is in the same directory as the conftest.py file. + """ + assert ( + ipg_obj.output_dir_path == os.path.abspath(os.path.dirname(__file__)) + "/files" + ) + assert ipg_obj.output_file_prefix == "raptor-tp6-amazon-firefox_powerlog" + + +def test_ipg_get_output_file_path(ipg_obj): + """Tests that the output file path is constantly changing + based on the file_counter value. + """ + test_path = "/test_path/" + test_ext = ".txt" + ipg_obj._file_counter = 1 + ipg_obj._output_dir_path = test_path + ipg_obj._output_file_ext = test_ext + + for i in range(1, 6): + fpath = ipg_obj._get_output_file_path() + + assert fpath.startswith(test_path) + assert fpath.endswith(test_ext) + assert str(i) in fpath + + +def test_ipg_start_and_stop(ipg_obj): + """Tests that the IPG thread can start and stop properly.""" + + def subprocess_side_effect(*args, **kwargs): + time.sleep(1) + + with mock.patch("subprocess.check_output") as m: + m.side_effect = subprocess_side_effect + + # Start recording IPG measurements + ipg_obj.start_ipg() + assert not ipg_obj._stop + + # Wait a bit for thread to start, then check it + timeout = 10 + start = time.time() + while time.time() - start < timeout and not ipg_obj._running: + time.sleep(1) + + assert ipg_obj._running + assert thread_is_alive(ipg_obj._thread) + + # Stop recording IPG measurements + ipg_obj.stop_ipg(wait_interval=1, timeout=30) + assert ipg_obj._stop + assert not ipg_obj._running + + +def test_ipg_stopping_timeout(ipg_obj): + """Tests that an IPGTimeoutError is raised when + the thread is still "running" and the wait in _wait_for_ipg + has exceeded the timeout value. + """ + with pytest.raises(IPGTimeoutError): + ipg_obj._running = True + ipg_obj._wait_for_ipg(wait_interval=1, timeout=2) + + +def test_ipg_rh_combine_cumulatives(ipg_rh_obj): + """Tests that cumulatives are correctly combined in + the _combine_cumulative_rows function. + """ + cumulatives_to_combine = [ + [0, 1, 2, 3, 4, 5], + [0, 1, 2, 3, 4, 5], + [0, 1, 2, 3, 4, 5], + [0, 1, 2, 3, 4, 5], + ] + + combined_cumulatives = ipg_rh_obj._combine_cumulative_rows(cumulatives_to_combine) + + # Check that accumulation worked, final value must be the maximum + assert combined_cumulatives[-1] == max(combined_cumulatives) + + # Check that the cumulative values are monotonically increasing + for count, val in enumerate(combined_cumulatives[:-1]): + assert combined_cumulatives[count + 1] - val >= 0 + + +def test_ipg_rh_clean_file(ipg_rh_obj): + """Tests that IPGResultsHandler correctly cleans the data + from one file. + """ + file = ipg_rh_obj._output_files[0] + linecount = 0 + with open(file, "r") as f: + for line in f: + linecount += 1 + + results, summary, clean_file = ipg_rh_obj._clean_ipg_file(file) + + # Check that each measure from the csv header + # is in the results dict and that the clean file output + # exists. + for measure in results: + assert measure in ipg_rh_obj._csv_header + assert os.path.exists(clean_file) + + clean_rows = [] + with open(clean_file, "r") as f: + for line in f: + if line.strip(): + clean_rows.append(line) + + # Make sure that the results and summary entries + # have the expected lengths. + for measure in results: + # Add 6 for new lines that were removed + assert len(results[measure]) + len(summary) + 6 == linecount + # Subtract 1 for the csv header + assert len(results[measure]) == len(clean_rows) - 1 + + +def test_ipg_rh_clean_ipg_data_no_files(ipg_rh_obj): + """Tests that IPGResultsHandler correctly handles the case + when no output files exist. + """ + ipg_rh_obj._output_files = [] + clean_data = ipg_rh_obj.clean_ipg_data() + assert clean_data is None + + +def test_ipg_rh_clean_ipg_data(ipg_rh_obj): + """Tests that IPGResultsHandler correctly handles cleaning + all known files and that the results and the merged output + are correct. + """ + clean_data = ipg_rh_obj.clean_ipg_data() + clean_files = ipg_rh_obj.cleaned_files + merged_output_path = ipg_rh_obj.merged_output_path + + # Check that the expected output exists + assert clean_data is not None + assert len(clean_files) == len(ipg_rh_obj._output_files) + assert os.path.exists(merged_output_path) + + # Check that the merged file length and results length + # is correct, and that no lines were lost and no extra lines + # were added. + expected_merged_line_count = 0 + for file in clean_files: + with open(file, "r") as f: + for count, line in enumerate(f): + if count == 0: + continue + if line.strip(): + expected_merged_line_count += 1 + + merged_line_count = 0 + with open(merged_output_path, "r") as f: + for count, line in enumerate(f): + if count == 0: + continue + if line.strip(): + merged_line_count += 1 + + assert merged_line_count == expected_merged_line_count + for measure in clean_data: + assert len(clean_data[measure]) == merged_line_count + + # Check that the clean data rows are ordered in increasing time + times_in_seconds = [] + for sys_time in clean_data["System Time"]: + split_sys_time = sys_time.split(":") + hour_min_sec = ":".join(split_sys_time[:-1]) + millis = float(split_sys_time[-1]) / 1000 + + timestruct = time.strptime(hour_min_sec, "%H:%M:%S") + times_in_seconds.append( + datetime.timedelta( + hours=timestruct.tm_hour, + minutes=timestruct.tm_min, + seconds=timestruct.tm_sec, + ).total_seconds() + + millis + ) + + for count, val in enumerate(times_in_seconds[:-1]): + assert times_in_seconds[count + 1] - val >= 0 + + +def test_ipg_rh_format_to_perfherder_with_no_results(ipg_rh_obj): + """Tests that formatting the data to a perfherder-like format + fails when clean_ipg_data was not called beforehand. + """ + formatted_data = ipg_rh_obj.format_ipg_data_to_partial_perfherder( + 1000, ipg_rh_obj._output_file_prefix + ) + assert formatted_data is None + + +def test_ipg_rh_format_to_perfherder_without_cutoff(ipg_rh_obj): + """Tests that formatting the data to a perfherder-like format + works as expected. + """ + ipg_rh_obj.clean_ipg_data() + formatted_data = ipg_rh_obj.format_ipg_data_to_partial_perfherder( + 1000, ipg_rh_obj._output_file_prefix + ) + + # Check that the expected entries exist + assert len(formatted_data.keys()) == 5 + assert "utilization" in formatted_data and "power-usage" in formatted_data + + assert ( + formatted_data["power-usage"]["test"] + == ipg_rh_obj._output_file_prefix + "-cumulative" + ) + assert ( + formatted_data["utilization"]["test"] + == ipg_rh_obj._output_file_prefix + "-utilization" + ) + assert ( + formatted_data["frequency-gpu"]["test"] + == ipg_rh_obj._output_file_prefix + "-frequency-gpu" + ) + assert ( + formatted_data["frequency-cpu"]["test"] + == ipg_rh_obj._output_file_prefix + "-frequency-cpu" + ) + assert ( + formatted_data["power-watts"]["test"] + == ipg_rh_obj._output_file_prefix + "-watts" + ) + + for measure in formatted_data: + # Make sure that the data exists + assert len(formatted_data[measure]["values"]) >= 1 + + for valkey in formatted_data[measure]["values"]: + # Make sure the names were simplified + assert "(" not in valkey + assert ")" not in valkey + + # Check that gpu utilization doesn't exist but cpu does + utilization_vals = formatted_data["utilization"]["values"] + assert "cpu" in utilization_vals + assert "gpu" not in utilization_vals + + expected_fields = ["processor-cores", "processor-package", "gpu", "dram"] + consumption_vals = formatted_data["power-usage"]["values"] + + consumption_vals_measures = list(consumption_vals.keys()) + + # This assertion ensures that the consumption values contain the expected + # fields and nothing more. + assert not list(set(consumption_vals_measures) - set(expected_fields)) + + +def test_ipg_rh_format_to_perfherder_with_cutoff(ipg_rh_obj): + """Tests that formatting the data to a perfherder-like format + works as expected. + """ + ipg_rh_obj.clean_ipg_data() + formatted_data = ipg_rh_obj.format_ipg_data_to_partial_perfherder( + 2.5, ipg_rh_obj._output_file_prefix + ) + + # Check that the formatted data was cutoff at the correct point, + # expecting that only the first row of merged will exist. + utilization_vals = formatted_data["utilization"]["values"] + assert utilization_vals["cpu"] == 14 + + # Expected vals are ordered in this way: [processor, cores, dram, gpu] + expected_vals = [6.517, 5.847, 0.244, 0.006] + consumption_vals = [ + formatted_data["power-usage"]["values"][measure] + for measure in formatted_data["power-usage"]["values"] + ] + assert not list(set(expected_vals) - set(consumption_vals)) + + +def test_ipg_rh_missingoutputfile(ipg_rh_obj): + """Tests that the IPGMissingOutputFileError is raised + when a bad file path is passed to _clean_ipg_file. + """ + bad_files = ["non-existent-file"] + with pytest.raises(IPGMissingOutputFileError): + ipg_rh_obj._clean_ipg_file(bad_files[0]) + + ipg_rh_obj._output_files = bad_files + with pytest.raises(IPGMissingOutputFileError): + ipg_rh_obj.clean_ipg_data() + + +def test_ipg_rh_emptyfile(ipg_rh_obj): + """Tests that the empty file error is raised when + a file exists, but does not contain any results in + it. + """ + base_path = os.path.abspath(os.path.dirname(__file__)) + "/files/" + bad_files = [base_path + "emptyfile.txt"] + with pytest.raises(IPGEmptyFileError): + ipg_rh_obj._clean_ipg_file(bad_files[0]) + + ipg_rh_obj._output_files = bad_files + with pytest.raises(IPGEmptyFileError): + ipg_rh_obj.clean_ipg_data() + + +def test_ipg_rh_valuetypeerrorfile(ipg_rh_obj): + """Tests that the IPGUnknownValueTypeError is raised + when a bad entry is encountered in a file that is cleaned. + """ + base_path = os.path.abspath(os.path.dirname(__file__)) + "/files/" + bad_files = [base_path + "valueerrorfile.txt"] + with pytest.raises(IPGUnknownValueTypeError): + ipg_rh_obj._clean_ipg_file(bad_files[0]) + + ipg_rh_obj._output_files = bad_files + with pytest.raises(IPGUnknownValueTypeError): + ipg_rh_obj.clean_ipg_data() + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozpower/tests/test_macintelpower.py b/testing/mozbase/mozpower/tests/test_macintelpower.py new file mode 100644 index 0000000000..2332c94c3e --- /dev/null +++ b/testing/mozbase/mozpower/tests/test_macintelpower.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +import time +from unittest import mock + +import mozunit + + +def test_macintelpower_init(macintelpower_obj): + """Tests that the MacIntelPower object is correctly initialized.""" + assert macintelpower_obj.ipg_path + assert macintelpower_obj.ipg + assert macintelpower_obj._os == "darwin" + assert macintelpower_obj._cpu == "intel" + + +def test_macintelpower_measuring(macintelpower_obj): + """Tests that measurement initialization and finalization works + for the MacIntelPower object. + """ + assert not macintelpower_obj.start_time + assert not macintelpower_obj.ipg._running + assert not macintelpower_obj.ipg._output_files + macintelpower_obj.initialize_power_measurements() + + # Check that initialization set start_time, and started the + # IPG measurer thread. + + # Wait a bit for thread to start, then check it + timeout = 10 + start = time.time() + while time.time() - start < timeout and not macintelpower_obj.ipg._running: + time.sleep(1) + + assert macintelpower_obj.start_time + assert macintelpower_obj.ipg._running + + test_data = {"power-usage": "data"} + + def formatter_side_effect(*args, **kwargs): + return test_data + + with mock.patch( + "mozpower.intel_power_gadget.IPGResultsHandler.clean_ipg_data" + ) as _: + with mock.patch( + "mozpower.intel_power_gadget.IPGResultsHandler." + "format_ipg_data_to_partial_perfherder" + ) as formatter: + formatter.side_effect = formatter_side_effect + + macintelpower_obj.finalize_power_measurements(wait_interval=2, timeout=30) + + # Check that finalization set the end_time, stopped the IPG measurement + # thread, added atleast one output file name, and initialized + # an IPGResultsHandler object + assert macintelpower_obj.end_time + assert not macintelpower_obj.ipg._running + assert macintelpower_obj.ipg._output_files + assert macintelpower_obj.ipg_results_handler + + # Check that the IPGResultHandler's methods were + # called + macintelpower_obj.ipg_results_handler.clean_ipg_data.assert_called() + macintelpower_obj.ipg_results_handler.format_ipg_data_to_partial_perfherder.assert_called_once_with( # NOQA: E501 + macintelpower_obj.end_time - macintelpower_obj.start_time, + "power-testing", + ) + + # Make sure we can get the expected perfherder data + # after formatting + assert macintelpower_obj.get_perfherder_data() == test_data + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozpower/tests/test_mozpower.py b/testing/mozbase/mozpower/tests/test_mozpower.py new file mode 100644 index 0000000000..fd513e836b --- /dev/null +++ b/testing/mozbase/mozpower/tests/test_mozpower.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python + +import subprocess +import sys +from unittest import mock + +import mozunit +import pytest +from mozpower import MozPower +from mozpower.mozpower import MissingProcessorInfoError, OsCpuComboMissingError + + +def test_mozpower_android_init_failure(): + """Tests that the MozPower object fails when the android + flag is set. Remove this test once android is implemented. + """ + with pytest.raises(NotImplementedError): + MozPower(android=True) + + +def test_mozpower_oscpu_combo_missing_error(): + """Tests that the error OsCpuComboMissingError is raised + when we can't find a OS, and CPU combination (and, therefore, cannot + find a power measurer). + """ + with mock.patch.object( + MozPower, "_get_os", return_value="Not-An-OS" + ) as _, mock.patch.object( + MozPower, "_get_processor_info", return_value="Not-A-Processor" + ) as _: + with pytest.raises(OsCpuComboMissingError): + MozPower() + + +def test_mozpower_processor_info_missing_error(): + """Tests that the error MissingProcessorInfoError is raised + when failures occur during processor information parsing. + """ + # The builtins module name differs between python 2 and 3 + builtins_name = "__builtin__" + if sys.version_info[0] == 3: + builtins_name = "builtins" + + def os_side_effect_true(*args, **kwargs): + """Used as a passing side effect for os.path.exists calls.""" + return True + + def os_side_effect_false(*args, **kwargs): + """Used as a failing side effect for os.path.exists calls.""" + return False + + def subprocess_side_effect_fail(*args, **kwargs): + """Used to mock a failure in subprocess.check_output calls.""" + raise subprocess.CalledProcessError(1, "Testing failure") + + # Test failures in macos processor information parsing + with mock.patch.object(MozPower, "_get_os", return_value="Darwin") as _: + + with mock.patch("os.path.exists") as os_mock: + os_mock.side_effect = os_side_effect_false + + # Check that we fail properly if the processor + # information file doesn't exist. + with pytest.raises(MissingProcessorInfoError): + MozPower() + + # Check that we fail properly when an error occurs + # in the subprocess call. + os_mock.side_effect = os_side_effect_true + with mock.patch("subprocess.check_output") as subprocess_mock: + subprocess_mock.side_effect = subprocess_side_effect_fail + with pytest.raises(MissingProcessorInfoError): + MozPower() + + # Test failures in linux processor information parsing + with mock.patch.object(MozPower, "_get_os", return_value="Linux") as _: + + with mock.patch("os.path.exists") as os_mock: + os_mock.side_effect = os_side_effect_false + + # Check that we fail properly if the processor + # information file doesn't exist. + with pytest.raises(MissingProcessorInfoError): + MozPower() + + # Check that we fail properly when the model cannot be found + # with by searching for 'model name'. + os_mock.side_effect = os_side_effect_true + with mock.patch( + "%s.open" % builtins_name, mock.mock_open(read_data="") + ) as _: + with pytest.raises(MissingProcessorInfoError): + MozPower() + + +def test_mozpower_oscpu_combo(mozpower_obj): + """Tests that the correct class is instantiated for a given + OS and CPU combination (MacIntelPower in this case). + """ + assert mozpower_obj.measurer.__class__.__name__ == "MacIntelPower" + assert ( + mozpower_obj.measurer._os == "darwin" and mozpower_obj.measurer._cpu == "intel" + ) + + +def test_mozpower_measuring(mozpower_obj): + """Tests that measurers are properly called with each method.""" + with mock.patch( + "mozpower.macintelpower.MacIntelPower.initialize_power_measurements" + ) as _, mock.patch( + "mozpower.macintelpower.MacIntelPower.finalize_power_measurements" + ) as _, mock.patch( + "mozpower.macintelpower.MacIntelPower.get_perfherder_data" + ) as _: + mozpower_obj.initialize_power_measurements() + mozpower_obj.measurer.initialize_power_measurements.assert_called() + + mozpower_obj.finalize_power_measurements() + mozpower_obj.measurer.finalize_power_measurements.assert_called() + + mozpower_obj.get_perfherder_data() + mozpower_obj.measurer.get_perfherder_data.assert_called() + + +def test_mozpower_measuring_with_no_measurer(mozpower_obj): + """Tests that no errors occur when the measurer is None, and the + initialize, finalize, and get_perfherder_data functions are called. + """ + with mock.patch( + "mozpower.macintelpower.MacIntelPower.initialize_power_measurements" + ) as _, mock.patch( + "mozpower.macintelpower.MacIntelPower.finalize_power_measurements" + ) as _, mock.patch( + "mozpower.macintelpower.MacIntelPower.get_perfherder_data" + ) as _: + measurer = mozpower_obj.measurer + mozpower_obj.measurer = None + + mozpower_obj.initialize_power_measurements() + assert not measurer.initialize_power_measurements.called + + mozpower_obj.finalize_power_measurements() + assert not measurer.finalize_power_measurements.called + + mozpower_obj.get_perfherder_data() + assert not measurer.get_perfherder_data.called + + mozpower_obj.get_full_perfherder_data("mozpower") + assert not measurer.get_perfherder_data.called + + +def test_mozpower_get_full_perfherder_data(mozpower_obj): + """Tests that the full perfherder data blob is properly + produced given a partial perfherder data blob with correct + entries. + """ + partial_perfherder = { + "utilization": { + "type": "power", + "test": "mozpower", + "unit": "%", + "values": {"cpu": 50, "gpu": 0}, + }, + "power-usage": { + "type": "power", + "test": "mozpower", + "unit": "mWh", + "values": {"cpu": 2.0, "dram": 0.1, "gpu": 4.0}, + }, + "frequency-cpu": { + "type": "power", + "test": "mozpower", + "unit": "MHz", + "values": { + "cpu-favg": 2.0, + "cpu-fmax": 5.0, + "cpu-fmin": 0.0, + }, + }, + "frequency-gpu": { + "type": "power", + "test": "mozpower", + "unit": "MHz", + "values": {"gpu-favg": 3.0, "gpu-fmax": 6.0, "gpu-fmin": 0.0}, + }, + } + utilization_vals = [0, 50] + power_usage_vals = [2.0, 0.1, 4.0] + frequency_cpu_vals = [2.0, 5.0, 0.0] + frequency_gpu_vals = [3.0, 6.0, 0.0] + + with mock.patch("mozpower.macintelpower.MacIntelPower.get_perfherder_data") as gpd: + gpd.return_value = partial_perfherder + + full_perfherder = mozpower_obj.get_full_perfherder_data("mozpower") + assert full_perfherder["framework"]["name"] == "mozpower" + assert len(full_perfherder["suites"]) == 4 + + # Check that each of the two suites were created correctly. + suites = full_perfherder["suites"] + for suite in suites: + assert "subtests" in suite + + assert suite["type"] == "power" + assert suite["alertThreshold"] == 2.0 + assert suite["lowerIsBetter"] + + all_vals = [] + for subtest in suite["subtests"]: + assert "value" in subtest + + # Check that the subtest names were created correctly + if "utilization" in suite["name"]: + assert "utilization" in subtest["name"] + elif "power-usage" in suite["name"]: + assert "power-usage" in subtest["name"] + elif "frequency-cpu" in suite["name"]: + assert "frequency-cpu" in subtest["name"] + elif "frequency-gpu" in suite["name"]: + assert "frequency-gpu" in subtest["name"] + else: + assert False, "Unknown subtest name %s" % subtest["name"] + + all_vals.append(subtest["value"]) + + if "utilization" in suite["name"]: + assert len(all_vals) == 2 + assert suite["unit"] == "%" + assert suite["name"] == "mozpower-utilization" + assert not list(set(all_vals) - set(utilization_vals)) + assert suite["value"] == float(25) + elif "power-usage" in suite["name"]: + assert len(all_vals) == 3 + assert suite["unit"] == "mWh" + assert suite["name"] == "mozpower-power-usage" + assert not list(set(all_vals) - set(power_usage_vals)) + assert suite["value"] == float(6.1) + elif "frequency-cpu" in suite["name"]: + assert len(all_vals) == 3 + assert suite["unit"] == "MHz" + assert suite["name"] == "mozpower-frequency-cpu" + assert not list(set(all_vals) - set(frequency_cpu_vals)) + assert suite["value"] == float(2.0) + elif "frequency-gpu" in suite["name"]: + assert len(all_vals) == 3 + assert suite["unit"] == "MHz" + assert suite["name"] == "mozpower-frequency-gpu" + assert not list(set(all_vals) - set(frequency_gpu_vals)) + assert suite["value"] == float(3.0) + else: + assert False, "Unknown suite name %s" % suite["name"] + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozpower/tests/test_powerbase.py b/testing/mozbase/mozpower/tests/test_powerbase.py new file mode 100644 index 0000000000..46de941457 --- /dev/null +++ b/testing/mozbase/mozpower/tests/test_powerbase.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +from unittest import mock + +import mozunit +import pytest +from mozpower.powerbase import ( + IPGExecutableMissingError, + PlatformUnsupportedError, + PowerBase, +) + + +def test_powerbase_intialization(): + """Tests that the PowerBase class correctly raises + a NotImplementedError when attempting to instantiate + it directly. + """ + with pytest.raises(NotImplementedError): + PowerBase() + + +def test_powerbase_missing_methods(powermeasurer): + """Tests that trying to call PowerBase methods + without an implementation in the subclass raises + the NotImplementedError. + """ + with pytest.raises(NotImplementedError): + powermeasurer.initialize_power_measurements() + + with pytest.raises(NotImplementedError): + powermeasurer.finalize_power_measurements() + + with pytest.raises(NotImplementedError): + powermeasurer.get_perfherder_data() + + +def test_powerbase_get_ipg_path_mac(powermeasurer): + """Tests that getting the IPG path returns the expected results.""" + powermeasurer._os = "darwin" + powermeasurer._cpu = "not-intel" + + def os_side_effect(arg): + """Used to get around the os.path.exists check in + get_ipg_path which raises an IPGExecutableMissingError + otherwise. + """ + return True + + with mock.patch("os.path.exists", return_value=True) as m: + m.side_effect = os_side_effect + + # None is returned when a non-intel based machine is + # tested. + ipg_path = powermeasurer.get_ipg_path() + assert ipg_path is None + + # Check the path returned for mac intel-based machines. + powermeasurer._cpu = "intel" + ipg_path = powermeasurer.get_ipg_path() + assert ipg_path == "/Applications/Intel Power Gadget/PowerLog" + + +def test_powerbase_get_ipg_path_errors(powermeasurer): + """Tests that the appropriate error is output when calling + get_ipg_path with invalid/unsupported _os and _cpu entries. + """ + + powermeasurer._cpu = "intel" + powermeasurer._os = "Not-An-OS" + + def os_side_effect(arg): + """Used to force the error IPGExecutableMissingError to occur + (in case a machine running these tests is a mac, and has intel + power gadget installed). + """ + return False + + with pytest.raises(PlatformUnsupportedError): + powermeasurer.get_ipg_path() + + with mock.patch("os.path.exists", return_value=False) as m: + m.side_effect = os_side_effect + + powermeasurer._os = "darwin" + with pytest.raises(IPGExecutableMissingError): + powermeasurer.get_ipg_path() + + +if __name__ == "__main__": + mozunit.main() |