text: # 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)
  
      if 'tasks' not in plist:
        logging.error("'tasks' field not found in plist.")
        return {}
  
      # The following CPU metrics are already time-normalized, and segmented by
      # process. Sum the metrics across all Chrome processes.
      cputime = 0
      energy_impact = 0
      browser_process_count = 0
      idle_wakeups = 0
      for task in plist['tasks']:
        if 'Chrome' in task['name'] or 'Chromium' in task['name']:
          if 'Helper' not in task['name']:
            browser_process_count += 1
          cputime += float(task['cputime_ms_per_s'])
          energy_impact += float(task.get('energy_impact', 0))
          idle_wakeups += float(task['idle_wakeups_per_s'])
      if browser_process_count == 0:
        logging.warning('No Chrome or Chromium browser process found with '
                        'powermetrics. Chrome CPU metrics will not be emitted.')
        return {}
      elif browser_process_count >= 2:
        logging.warning('powermetrics found more than one Chrome or Chromium '
                        'browser. Chrome CPU metrics will not be emitted.')
        # During Telemetry unit tests, there may be multiple Chrome browsers
        # present. Don't add cpu metrics, but don't return {} either.
      else:  # browser_process_count == 1:
        chrome_dict = {}
        chrome_dict['cputime_ms_per_s'] = cputime
        chrome_dict['energy_impact'] = energy_impact
        chrome_dict['idle_wakeups_per_s'] = idle_wakeups
        out_dict['component_utilization']['chrome'] = chrome_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
  