| Index: tools/telemetry/telemetry/internal/platform/power_monitor/powermetrics_power_monitor.py
|
| diff --git a/tools/telemetry/telemetry/internal/platform/power_monitor/powermetrics_power_monitor.py b/tools/telemetry/telemetry/internal/platform/power_monitor/powermetrics_power_monitor.py
|
| deleted file mode 100644
|
| index aa8687651cbc5c31e4156442f244b7c29d32ff46..0000000000000000000000000000000000000000
|
| --- a/tools/telemetry/telemetry/internal/platform/power_monitor/powermetrics_power_monitor.py
|
| +++ /dev/null
|
| @@ -1,281 +0,0 @@
|
| -# Copyright 2014 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -import collections
|
| -import logging
|
| -import os
|
| -import plistlib
|
| -import shutil
|
| -import tempfile
|
| -import xml.parsers.expat
|
| -
|
| -from telemetry.core import os_version
|
| -from telemetry.core import util
|
| -from telemetry import decorators
|
| -from telemetry.internal.platform import power_monitor
|
| -
|
| -
|
| -# TODO: rename this class (seems like this is used by mac)
|
| -class PowerMetricsPowerMonitor(power_monitor.PowerMonitor):
|
| -
|
| - def __init__(self, backend):
|
| - super(PowerMetricsPowerMonitor, self).__init__()
|
| - self._powermetrics_process = None
|
| - self._backend = backend
|
| - self._output_filename = None
|
| - self._output_directory = None
|
| -
|
| - @property
|
| - def binary_path(self):
|
| - return '/usr/bin/powermetrics'
|
| -
|
| - def StartMonitoringPower(self, browser):
|
| - self._CheckStart()
|
| - # Empirically powermetrics creates an empty output file immediately upon
|
| - # starting. We detect file creation as a signal that measurement has
|
| - # started. In order to avoid various race conditions in tempfile creation
|
| - # we create a temp directory and have powermetrics create it's output
|
| - # there rather than say, creating a tempfile, deleting it and reusing its
|
| - # name.
|
| - self._output_directory = tempfile.mkdtemp()
|
| - self._output_filename = os.path.join(self._output_directory,
|
| - 'powermetrics.output')
|
| - args = ['-f', 'plist',
|
| - '-u', self._output_filename,
|
| - '-i0',
|
| - '--show-usage-summary']
|
| - self._powermetrics_process = self._backend.LaunchApplication(
|
| - self.binary_path, args, elevate_privilege=True)
|
| -
|
| - # Block until output file is written to ensure this function call is
|
| - # synchronous in respect to powermetrics starting.
|
| - def _OutputFileExists():
|
| - return os.path.isfile(self._output_filename)
|
| - util.WaitFor(_OutputFileExists, 1)
|
| -
|
| - @decorators.Cache
|
| - def CanMonitorPower(self):
|
| - mavericks_or_later = (
|
| - self._backend.GetOSVersionName() >= os_version.MAVERICKS)
|
| - binary_path = self.binary_path
|
| - return mavericks_or_later and self._backend.CanLaunchApplication(
|
| - binary_path)
|
| -
|
| - @staticmethod
|
| - def _ParsePlistString(plist_string):
|
| - """Wrapper to parse a plist from a string and catch any errors.
|
| -
|
| - Sometimes powermetrics will exit in the middle of writing it's output,
|
| - empirically it seems that it always writes at least one sample in it's
|
| - entirety so we can safely ignore any errors in it's output.
|
| -
|
| - Returns:
|
| - Parser output on successful parse, None on parse error.
|
| - """
|
| - try:
|
| - return plistlib.readPlistFromString(plist_string)
|
| - except xml.parsers.expat.ExpatError:
|
| - return None
|
| -
|
| - @staticmethod
|
| - def ParsePowerMetricsOutput(powermetrics_output):
|
| - """Parse output of powermetrics command line utility.
|
| -
|
| - Returns:
|
| - Dictionary in the format returned by StopMonitoringPower() or None
|
| - if |powermetrics_output| is empty - crbug.com/353250 .
|
| - """
|
| - if len(powermetrics_output) == 0:
|
| - logging.warning('powermetrics produced zero length output')
|
| - return {}
|
| -
|
| - # Container to collect samples for running averages.
|
| - # out_path - list containing the key path in the output dictionary.
|
| - # src_path - list containing the key path to get the data from in
|
| - # powermetrics' output.
|
| - def ConstructMetric(out_path, src_path):
|
| - RunningAverage = collections.namedtuple('RunningAverage', [
|
| - 'out_path', 'src_path', 'samples'])
|
| - return RunningAverage(out_path, src_path, [])
|
| -
|
| - # List of RunningAverage objects specifying metrics we want to aggregate.
|
| - metrics = [
|
| - ConstructMetric(
|
| - ['platform_info', 'average_frequency_hz'],
|
| - ['processor', 'freq_hz']),
|
| - ConstructMetric(
|
| - ['platform_info', 'idle_percent'],
|
| - ['processor', 'packages', 0, 'c_state_ratio'])]
|
| -
|
| - def DataWithMetricKeyPath(metric, powermetrics_output):
|
| - """Retrieve the sample from powermetrics' output for a given metric.
|
| -
|
| - Args:
|
| - metric: The RunningAverage object we want to collect a new sample for.
|
| - powermetrics_output: Dictionary containing powermetrics output.
|
| -
|
| - Returns:
|
| - The sample corresponding to |metric|'s keypath."""
|
| - # Get actual data corresponding to key path.
|
| - out_data = powermetrics_output
|
| - for k in metric.src_path:
|
| - out_data = out_data[k]
|
| -
|
| - assert type(out_data) in [int, float], (
|
| - 'Was expecting a number: %s (%s)' % (type(out_data), out_data))
|
| - return float(out_data)
|
| -
|
| - sample_durations = []
|
| - total_energy_consumption_mwh = 0
|
| - # powermetrics outputs multiple plists separated by null terminators.
|
| - raw_plists = powermetrics_output.split('\0')
|
| - raw_plists = [x for x in raw_plists if len(x) > 0]
|
| - assert len(raw_plists) == 1
|
| -
|
| - # -------- Examine contents of first plist for systems specs. --------
|
| - plist = PowerMetricsPowerMonitor._ParsePlistString(raw_plists[0])
|
| - if not plist:
|
| - logging.warning('powermetrics produced invalid output, output length: '
|
| - '%d', len(powermetrics_output))
|
| - return {}
|
| -
|
| - # Powermetrics doesn't record power usage when running on a VM.
|
| - hw_model = plist.get('hw_model')
|
| - if hw_model and hw_model.startswith('VMware'):
|
| - return {}
|
| -
|
| - if 'GPU' in plist:
|
| - metrics.extend([
|
| - ConstructMetric(
|
| - ['component_utilization', 'gpu', 'average_frequency_hz'],
|
| - ['GPU', 0, 'freq_hz']),
|
| - ConstructMetric(
|
| - ['component_utilization', 'gpu', 'idle_percent'],
|
| - ['GPU', 0, 'c_state_ratio'])])
|
| -
|
| - # There's no way of knowing ahead of time how many cpus and packages the
|
| - # current system has. Iterate over cores and cpus - construct metrics for
|
| - # each one.
|
| - if 'processor' in plist:
|
| - core_dict = plist['processor']['packages'][0]['cores']
|
| - num_cores = len(core_dict)
|
| - cpu_num = 0
|
| - for core_idx in xrange(num_cores):
|
| - num_cpus = len(core_dict[core_idx]['cpus'])
|
| - base_src_path = ['processor', 'packages', 0, 'cores', core_idx]
|
| - for cpu_idx in xrange(num_cpus):
|
| - base_out_path = ['component_utilization', 'cpu%d' % cpu_num]
|
| - # C State ratio is per-package, component CPUs of that package may
|
| - # have different frequencies.
|
| - metrics.append(ConstructMetric(
|
| - base_out_path + ['average_frequency_hz'],
|
| - base_src_path + ['cpus', cpu_idx, 'freq_hz']))
|
| - metrics.append(ConstructMetric(
|
| - base_out_path + ['idle_percent'],
|
| - base_src_path + ['c_state_ratio']))
|
| - cpu_num += 1
|
| -
|
| - # -------- Parse Data Out of Plists --------
|
| - plist = PowerMetricsPowerMonitor._ParsePlistString(raw_plists[0])
|
| - if not plist:
|
| - logging.error('Error parsing plist.')
|
| - return {}
|
| -
|
| - # Duration of this sample.
|
| - sample_duration_ms = int(plist['elapsed_ns']) / 10 ** 6
|
| - sample_durations.append(sample_duration_ms)
|
| -
|
| - if 'processor' not in plist:
|
| - logging.error("'processor' field not found in plist.")
|
| - return {}
|
| - processor = plist['processor']
|
| -
|
| - total_energy_consumption_mwh = (
|
| - (float(processor.get('package_joules', 0)) / 3600.) * 10 ** 3)
|
| -
|
| - for m in metrics:
|
| - try:
|
| - m.samples.append(DataWithMetricKeyPath(m, plist))
|
| - except KeyError:
|
| - # Old CPUs don't have c-states, so if data is missing, just ignore it.
|
| - logging.info('Field missing from powermetrics output: %s', m.src_path)
|
| - continue
|
| -
|
| - # -------- Collect and Process Data --------
|
| - out_dict = {}
|
| - out_dict['identifier'] = 'powermetrics'
|
| - out_dict['energy_consumption_mwh'] = total_energy_consumption_mwh
|
| -
|
| - def StoreMetricAverage(metric, sample_durations, out):
|
| - """Calculate average value of samples in a metric and store in output
|
| - path as specified by metric.
|
| -
|
| - Args:
|
| - metric: A RunningAverage object containing samples to average.
|
| - sample_durations: A list which parallels the samples list containing
|
| - the time slice for each sample.
|
| - out: The output dicat, average is stored in the location specified by
|
| - metric.out_path.
|
| - """
|
| - if len(metric.samples) == 0:
|
| - return
|
| -
|
| - assert len(metric.samples) == len(sample_durations)
|
| - avg = 0
|
| - for i in xrange(len(metric.samples)):
|
| - avg += metric.samples[i] * sample_durations[i]
|
| - avg /= sum(sample_durations)
|
| -
|
| - # Store data in output, creating empty dictionaries as we go.
|
| - for k in metric.out_path[:-1]:
|
| - if not out.has_key(k):
|
| - out[k] = {}
|
| - out = out[k]
|
| - out[metric.out_path[-1]] = avg
|
| -
|
| - for m in metrics:
|
| - StoreMetricAverage(m, sample_durations, out_dict)
|
| - return out_dict
|
| -
|
| - def _KillPowerMetricsProcess(self):
|
| - """Kill a running powermetrics process."""
|
| - try:
|
| - if self._powermetrics_process.poll() is None:
|
| - self._powermetrics_process.terminate()
|
| - except OSError as e:
|
| - logging.warning(
|
| - 'Error when trying to terminate powermetric process: %s', repr(e))
|
| - if self._powermetrics_process.poll() is None:
|
| - # terminate() can fail when Powermetrics does not have the SetUID set.
|
| - self._backend.LaunchApplication(
|
| - '/usr/bin/pkill',
|
| - ['-SIGTERM', os.path.basename(self.binary_path)],
|
| - elevate_privilege=True)
|
| -
|
| - def StopMonitoringPower(self):
|
| - self._CheckStop()
|
| - # Tell powermetrics to take an immediate sample.
|
| - try:
|
| - self._KillPowerMetricsProcess()
|
| - (power_stdout, power_stderr) = self._powermetrics_process.communicate()
|
| - returncode = self._powermetrics_process.returncode
|
| - assert returncode in [0, -15], (
|
| - """powermetrics error
|
| - return code=%d
|
| - stdout=(%s)
|
| - stderr=(%s)""" % (returncode, power_stdout, power_stderr))
|
| -
|
| - with open(self._output_filename, 'rb') as output_file:
|
| - powermetrics_output = output_file.read()
|
| - return PowerMetricsPowerMonitor.ParsePowerMetricsOutput(
|
| - powermetrics_output)
|
| - except Exception as e:
|
| - logging.warning(
|
| - 'Error when trying to collect power monitoring data: %s', repr(e))
|
| - return PowerMetricsPowerMonitor.ParsePowerMetricsOutput('')
|
| - finally:
|
| - shutil.rmtree(self._output_directory)
|
| - self._output_directory = None
|
| - self._output_filename = None
|
| - self._powermetrics_process = None
|
|
|