summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozpower/mozpower/mozpower.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozpower/mozpower/mozpower.py')
-rw-r--r--testing/mozbase/mozpower/mozpower/mozpower.py376
1 files changed, 376 insertions, 0 deletions
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