Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(179)

Unified Diff: tools/telemetry/telemetry/core/platform/power_monitor/powermetrics_power_monitor.py

Issue 164133002: [Telemetry] Refactor mac power monitoring to use the base PowerMonitor class. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Follow review Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: tools/telemetry/telemetry/core/platform/power_monitor/powermetrics_power_monitor.py
diff --git a/tools/telemetry/telemetry/core/platform/power_monitor/powermetrics_power_monitor.py b/tools/telemetry/telemetry/core/platform/power_monitor/powermetrics_power_monitor.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e53af05f45a41439e7ae8c57adfe1207fadde20
--- /dev/null
+++ b/tools/telemetry/telemetry/core/platform/power_monitor/powermetrics_power_monitor.py
@@ -0,0 +1,256 @@
+# 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 signal
+import tempfile
+import xml.parsers.expat
+
+from telemetry import decorators
+from telemetry.core import util
+import telemetry.core.platform as platform
+import telemetry.core.platform.power_monitor as power_monitor
+
+
+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._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)
+
+ @decorators.Cache
+ def CanMonitorPowerAsync(self):
+ mavericks_or_later = (self._backend.GetOSVersionName() >=
+ platform.mac_platform_backend.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 succesful 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 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 = PowerMetricsPowerMonitor._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 = PowerMetricsPowerMonitor._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
+
+ 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:
+ powermetrics_output = output_file.read()
+ assert len(powermetrics_output) > 0
+ return PowerMetricsPowerMonitor.ParsePowerMetricsOutput(
+ powermetrics_output)
+
+ finally:
+ shutil.rmtree(self._ouput_directory)
+ self._ouput_directory = None
+ self._output_filename = None
+ self._powermetrics_process = None

Powered by Google App Engine
This is Rietveld 408576698