Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / platform / mac_platform_backend.py
index eb99577..b391e22 100644 (file)
@@ -2,28 +2,42 @@
 # 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 subprocess
 import tempfile
 import time
+import xml.parsers.expat
 try:
   import resource  # pylint: disable=F0401
 except ImportError:
   resource = None  # Not available on all platforms
 
-from ctypes import util
+from telemetry import decorators
+from telemetry.core import util
+from telemetry.core.platform import platform_backend
 from telemetry.core.platform import posix_platform_backend
 
+
+LEOPARD =      platform_backend.OSVersion('leopard',      10.5)
+SNOWLEOPARD =  platform_backend.OSVersion('snowleopard',  10.6)
+LION =         platform_backend.OSVersion('lion',         10.7)
+MOUNTAINLION = platform_backend.OSVersion('mountainlion', 10.8)
+MAVERICKS =    platform_backend.OSVersion('mavericks',    10.9)
+
+
 class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
 
   class PowerMetricsUtility(object):
-    def __init__(self):
+    def __init__(self, backend):
       self._powermetrics_process = None
-      self._powermetrics_output_file = None
+      self._backend = backend
+      self._output_filename = None
+      self._ouput_directory = None
 
     @property
     def binary_path(self):
@@ -33,18 +47,27 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
       assert not self._powermetrics_process, (
           "Must call StopMonitoringPowerAsync().")
       SAMPLE_INTERVAL_MS = 1000 / 20 # 20 Hz, arbitrary.
-      self._powermetrics_output_file = tempfile.NamedTemporaryFile()
-      args = [self.binary_path, '-f', 'plist', '-i',
-          '%d' % SAMPLE_INTERVAL_MS, '-u', self._powermetrics_output_file.name]
-
-      # powermetrics writes lots of output to stderr, don't echo unless verbose
-      # logging enabled.
-      stderror_destination = subprocess.PIPE
-      if logging.getLogger().isEnabledFor(logging.DEBUG):
-        stderror_destination = None
-
-      self._powermetrics_process = subprocess.Popen(args,
-          stdout=subprocess.PIPE, stderr=stderror_destination)
+      # 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, (
@@ -56,16 +79,19 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
         returncode = self._powermetrics_process.wait()
         assert returncode in [0, -15], (
             "powermetrics return code: %d" % returncode)
-        return open(self._powermetrics_output_file.name, 'r').read()
+
+        with open(self._output_filename, 'rb') as output_file:
+          return output_file.read()
       finally:
-        self._powermetrics_output_file.close()
-        self._powermetrics_output_file = None
+        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.powermetrics_tool_ = MacPlatformBackend.PowerMetricsUtility(self)
 
   def StartRawDisplayFrameRateMeasurement(self):
     raise NotImplementedError()
@@ -111,7 +137,7 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
 
     proc_info = ProcTaskInfo()
     if not self.libproc:
-      self.libproc = ctypes.CDLL(util.find_library('libproc'))
+      self.libproc = ctypes.CDLL(ctypes.util.find_library('libproc'))
     self.libproc.proc_pidinfo(pid, proc_info.PROC_PIDTASKINFO, 0,
                               ctypes.byref(proc_info), proc_info.size)
 
@@ -133,6 +159,10 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
         return pages_active * resource.getpagesize() / 1024
     return 0
 
+  @decorators.Cache
+  def GetSystemTotalPhysicalMemory(self):
+    return int(self._RunCommand(['sysctl', '-n', 'hw.memsize']))
+
   def PurgeUnpinnedMemory(self):
     # TODO(pliard): Implement this.
     pass
@@ -148,35 +178,37 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
   def GetOSName(self):
     return 'mac'
 
+  @decorators.Cache
   def GetOSVersionName(self):
     os_version = os.uname()[2]
 
     if os_version.startswith('9.'):
-      return 'leopard'
+      return LEOPARD
     if os_version.startswith('10.'):
-      return 'snowleopard'
+      return SNOWLEOPARD
     if os_version.startswith('11.'):
-      return 'lion'
+      return LION
     if os_version.startswith('12.'):
-      return 'mountainlion'
+      return MOUNTAINLION
     if os_version.startswith('13.'):
-      return 'mavericks'
+      return MAVERICKS
 
-    raise NotImplementedError("Unknown OS X version %s." % os_version)
+    raise NotImplementedError('Unknown mac version %s.' % os_version)
 
   def CanFlushIndividualFilesFromSystemCache(self):
     return False
 
   def FlushEntireSystemCache(self):
-    p = subprocess.Popen(['purge'])
+    mavericks_or_later = self.GetOSVersionName() >= MAVERICKS
+    p = self.LaunchApplication('purge', elevate_privilege=mavericks_or_later)
     p.wait()
     assert p.returncode == 0, 'Failed to flush system cache'
 
+  @decorators.Cache
   def CanMonitorPowerAsync(self):
-    # powermetrics only runs on OS X version >= 10.9 .
-    os_version = int(os.uname()[2].split('.')[0])
+    mavericks_or_later = self.GetOSVersionName() >= MAVERICKS
     binary_path = self.powermetrics_tool_.binary_path
-    return os_version >= 13 and self.CanLaunchApplication(binary_path)
+    return mavericks_or_later and self.CanLaunchApplication(binary_path)
 
   def SetPowerMetricsUtilityForTest(self, obj):
     self.powermetrics_tool_ = obj
@@ -184,21 +216,119 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
   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')[:-1]
+    # 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 = plistlib.readPlistFromString(raw_plist)
+      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
@@ -211,13 +341,46 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
 
       power_samples.append(energy_consumption_mw)
 
-    # -------- Collect and Process Data -------------
+      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):