| Index: tools/telemetry/telemetry/core/platform/mac_platform_backend.py
|
| diff --git a/tools/telemetry/telemetry/core/platform/mac_platform_backend.py b/tools/telemetry/telemetry/core/platform/mac_platform_backend.py
|
| index b391e221bf68f33b9f02bbd13635da3e7915a9bc..0e5cb42e82e513af51c99570bba947657d75e8b9 100644
|
| --- a/tools/telemetry/telemetry/core/platform/mac_platform_backend.py
|
| +++ b/tools/telemetry/telemetry/core/platform/mac_platform_backend.py
|
| @@ -2,25 +2,18 @@
|
| # Use of this source code is governed by a BSD-style license that can be
|
| # found in the LICENSE file.
|
|
|
| -import collections
|
| import ctypes
|
| -import logging
|
| import os
|
| -import plistlib
|
| -import shutil
|
| -import signal
|
| -import tempfile
|
| import time
|
| -import xml.parsers.expat
|
| try:
|
| import resource # pylint: disable=F0401
|
| except ImportError:
|
| resource = None # Not available on all platforms
|
|
|
| from telemetry import decorators
|
| -from telemetry.core import util
|
| from telemetry.core.platform import platform_backend
|
| from telemetry.core.platform import posix_platform_backend
|
| +from telemetry.core.platform.power_monitor import powermetrics_power_monitor
|
|
|
|
|
| LEOPARD = platform_backend.OSVersion('leopard', 10.5)
|
| @@ -31,67 +24,11 @@ MAVERICKS = platform_backend.OSVersion('mavericks', 10.9)
|
|
|
|
|
| class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
|
| -
|
| - class PowerMetricsUtility(object):
|
| - def __init__(self, backend):
|
| - self._powermetrics_process = None
|
| - self._backend = backend
|
| - self._output_filename = None
|
| - self._ouput_directory = None
|
| -
|
| - @property
|
| - def binary_path(self):
|
| - return '/usr/bin/powermetrics'
|
| -
|
| - def StartMonitoringPowerAsync(self):
|
| - assert not self._powermetrics_process, (
|
| - "Must call StopMonitoringPowerAsync().")
|
| - SAMPLE_INTERVAL_MS = 1000 / 20 # 20 Hz, arbitrary.
|
| - # 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._ouput_directory = tempfile.mkdtemp()
|
| - self._output_filename = os.path.join(self._ouput_directory,
|
| - 'powermetrics.output')
|
| - args = ['-f', 'plist',
|
| - '-i', '%d' % SAMPLE_INTERVAL_MS,
|
| - '-u', self._output_filename]
|
| - 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)
|
| - timeout_sec = 2 * (SAMPLE_INTERVAL_MS / 1000.)
|
| - util.WaitFor(_OutputFileExists, timeout_sec)
|
| -
|
| - def StopMonitoringPowerAsync(self):
|
| - assert self._powermetrics_process, (
|
| - "StartMonitoringPowerAsync() not called.")
|
| - # Tell powermetrics to take an immediate sample.
|
| - try:
|
| - self._powermetrics_process.send_signal(signal.SIGINFO)
|
| - self._powermetrics_process.send_signal(signal.SIGTERM)
|
| - returncode = self._powermetrics_process.wait()
|
| - assert returncode in [0, -15], (
|
| - "powermetrics return code: %d" % returncode)
|
| -
|
| - with open(self._output_filename, 'rb') as output_file:
|
| - return output_file.read()
|
| - finally:
|
| - shutil.rmtree(self._ouput_directory)
|
| - self._ouput_directory = None
|
| - self._output_filename = None
|
| - self._powermetrics_process = None
|
| -
|
| def __init__(self):
|
| super(MacPlatformBackend, self).__init__()
|
| self.libproc = None
|
| - self.powermetrics_tool_ = MacPlatformBackend.PowerMetricsUtility(self)
|
| + self.power_monitor_ = powermetrics_power_monitor.PowerMetricsPowerMonitor(
|
| + self)
|
|
|
| def StartRawDisplayFrameRateMeasurement(self):
|
| raise NotImplementedError()
|
| @@ -204,186 +141,11 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
|
| p.wait()
|
| assert p.returncode == 0, 'Failed to flush system cache'
|
|
|
| - @decorators.Cache
|
| def CanMonitorPowerAsync(self):
|
| - mavericks_or_later = self.GetOSVersionName() >= MAVERICKS
|
| - binary_path = self.powermetrics_tool_.binary_path
|
| - return mavericks_or_later and self.CanLaunchApplication(binary_path)
|
| -
|
| - def SetPowerMetricsUtilityForTest(self, obj):
|
| - self.powermetrics_tool_ = obj
|
| + return self.power_monitor_.CanMonitorPowerAsync()
|
|
|
| def StartMonitoringPowerAsync(self):
|
| - self.powermetrics_tool_.StartMonitoringPowerAsync()
|
| -
|
| - def _ParsePlistString(self, 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 succesful parse, None on parse error.
|
| - """
|
| - try:
|
| - return plistlib.readPlistFromString(plist_string)
|
| - except xml.parsers.expat.ExpatError:
|
| - return None
|
| -
|
| - def _ParsePowerMetricsOutput(self, powermetrics_output):
|
| - """Parse output of powermetrics command line utility.
|
| -
|
| - Returns:
|
| - Dictionary in the format returned by StopMonitoringPowerAsync().
|
| - """
|
| -
|
| - # 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(
|
| - ['component_utilization', 'whole_package', 'average_frequency_mhz'],
|
| - ['processor','freq_hz']),
|
| - ConstructMetric(
|
| - ['component_utilization', 'whole_package', '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)
|
| -
|
| - power_samples = []
|
| - 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]
|
| -
|
| - # -------- Examine contents of first plist for systems specs. --------
|
| - plist = self._ParsePlistString(raw_plists[0])
|
| - if not plist:
|
| - logging.warning("powermetrics produced invalid output, output length: "
|
| - "%d" % len(powermetrics_output))
|
| - return {}
|
| -
|
| - if 'GPU' in plist:
|
| - metrics.extend([
|
| - ConstructMetric(
|
| - ['component_utilization', 'gpu', 'average_frequency_mhz'],
|
| - ['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_mhz'],
|
| - 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 --------
|
| - for raw_plist in raw_plists:
|
| - plist = self._ParsePlistString(raw_plist)
|
| - if not plist:
|
| - continue
|
| -
|
| - # Duration of this sample.
|
| - sample_duration_ms = int(plist['elapsed_ns']) / 10**6
|
| - sample_durations.append(sample_duration_ms)
|
| -
|
| - if 'processor' not in plist:
|
| - continue
|
| - processor = plist['processor']
|
| -
|
| - energy_consumption_mw = int(processor.get('package_watts', 0)) * 10**3
|
| -
|
| - total_energy_consumption_mwh += (energy_consumption_mw *
|
| - (sample_duration_ms / 3600000.))
|
| -
|
| - power_samples.append(energy_consumption_mw)
|
| -
|
| - for m in metrics:
|
| - m.samples.append(DataWithMetricKeyPath(m, plist))
|
| -
|
| - # -------- Collect and Process Data --------
|
| - out_dict = {}
|
| - out_dict['identifier'] = 'powermetrics'
|
| - # Raw power usage samples.
|
| - if power_samples:
|
| - out_dict['power_samples_mw'] = power_samples
|
| - 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
|
| + self.power_monitor_.StartMonitoringPowerAsync()
|
|
|
| def StopMonitoringPowerAsync(self):
|
| - powermetrics_output = self.powermetrics_tool_.StopMonitoringPowerAsync()
|
| - assert len(powermetrics_output) > 0
|
| - return self._ParsePowerMetricsOutput(powermetrics_output)
|
| + return self.power_monitor_.StopMonitoringPowerAsync()
|
|
|