Home | History | Annotate | Download | only in metrics
      1 # Copyright 2014 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import time
      6 
      7 from metrics import Metric
      8 from telemetry.core.platform import process_statistic_timeline_data
      9 from telemetry.value import scalar
     10 
     11 
     12 class PowerMetric(Metric):
     13   """A metric for measuring power usage."""
     14 
     15   # System power draw while idle.
     16   _quiescent_power_draw_mwh = 0
     17 
     18   def __init__(self, platform, quiescent_measurement_time_s=0):
     19     """PowerMetric Constructor.
     20 
     21     Args:
     22         platform: platform object to use.
     23         quiescent_measurement_time_s: time to measure quiescent power,
     24             in seconds. 0 means don't measure quiescent power."""
     25     super(PowerMetric, self).__init__()
     26     self._browser = None
     27     self._platform = platform
     28     self._running = False
     29     self._starting_cpu_stats = None
     30     self._results = None
     31     self._MeasureQuiescentPower(quiescent_measurement_time_s)
     32 
     33   def __del__(self):
     34     # TODO(jeremy): Remove once crbug.com/350841 is fixed.
     35     # Don't leave power monitoring processes running on the system.
     36     self._StopInternal()
     37     parent = super(PowerMetric, self)
     38     if hasattr(parent, '__del__'):
     39       parent.__del__()
     40 
     41   def _StopInternal(self):
     42     """Stop monitoring power if measurement is running. This function is
     43     idempotent."""
     44     if not self._running:
     45       return
     46     self._running = False
     47     self._results = self._platform.StopMonitoringPower()
     48     if self._results: # StopMonitoringPower() can return None.
     49       self._results['cpu_stats'] = (
     50           _SubtractCpuStats(self._browser.cpu_stats, self._starting_cpu_stats))
     51 
     52   def _MeasureQuiescentPower(self, measurement_time_s):
     53     """Measure quiescent power draw for the system."""
     54     if not self._platform.CanMonitorPower() or \
     55         self._platform.CanMeasurePerApplicationPower() or \
     56         not measurement_time_s:
     57       return
     58 
     59     # Only perform quiescent measurement once per run.
     60     if PowerMetric._quiescent_power_draw_mwh:
     61       return
     62 
     63     self._platform.StartMonitoringPower(self._browser)
     64     time.sleep(measurement_time_s)
     65     power_results = self._platform.StopMonitoringPower()
     66     PowerMetric._quiescent_power_draw_mwh = (
     67         power_results.get('energy_consumption_mwh', 0))
     68 
     69   def Start(self, _, tab):
     70     self._browser = tab.browser
     71 
     72     if not self._platform.CanMonitorPower():
     73       return
     74 
     75     self._results = None
     76     self._StopInternal()
     77 
     78     # This line invokes top a few times, call before starting power measurement.
     79     self._starting_cpu_stats = self._browser.cpu_stats
     80     self._platform.StartMonitoringPower(self._browser)
     81     self._running = True
     82 
     83   def Stop(self, _, tab):
     84     if not self._platform.CanMonitorPower():
     85       return
     86 
     87     self._StopInternal()
     88 
     89   def AddResults(self, _, results):
     90     """Add the collected power data into the results object.
     91 
     92     This function needs to be robust in the face of differing power data on
     93     various platforms. Therefore data existence needs to be checked when
     94     building up the results. Additionally 0 is a valid value for many of the
     95     metrics here which is why there are plenty of checks for 'is not None'
     96     below.
     97     """
     98     if not self._results:
     99       return
    100 
    101     application_energy_consumption_mwh = (
    102         self._results.get('application_energy_consumption_mwh'))
    103     total_energy_consumption_mwh = self._results.get('energy_consumption_mwh')
    104 
    105     if not application_energy_consumption_mwh and total_energy_consumption_mwh:
    106       application_energy_consumption_mwh = max(
    107           total_energy_consumption_mwh - PowerMetric._quiescent_power_draw_mwh,
    108           0)
    109 
    110     if total_energy_consumption_mwh is not None:
    111       results.AddValue(scalar.ScalarValue(
    112           results.current_page, 'energy_consumption_mwh', 'mWh',
    113           total_energy_consumption_mwh))
    114 
    115     if application_energy_consumption_mwh is not None:
    116       results.AddValue(scalar.ScalarValue(
    117           results.current_page, 'application_energy_consumption_mwh', 'mWh',
    118           application_energy_consumption_mwh))
    119 
    120     component_utilization = self._results.get('component_utilization', {})
    121     # GPU Frequency.
    122     gpu_power = component_utilization.get('gpu', {})
    123     gpu_freq_hz = gpu_power.get('average_frequency_hz')
    124     if gpu_freq_hz is not None:
    125       results.AddValue(scalar.ScalarValue(
    126           results.current_page, 'gpu_average_frequency_hz', 'hz', gpu_freq_hz,
    127           important=False))
    128 
    129     # Add idle wakeup numbers for all processes.
    130     for (process_type, stats) in self._results.get('cpu_stats', {}).items():
    131       trace_name_for_process = 'idle_wakeups_%s' % (process_type.lower())
    132       results.AddValue(scalar.ScalarValue(
    133           results.current_page, trace_name_for_process, 'count', stats,
    134           important=False))
    135 
    136     # Add temperature measurements.
    137     whole_package_utilization = component_utilization.get('whole_package', {})
    138     board_temperature_c = whole_package_utilization.get('average_temperature_c')
    139     if board_temperature_c is not None:
    140       results.AddValue(scalar.ScalarValue(
    141           results.current_page, 'board_temperature', 'celsius',
    142           board_temperature_c, important=False))
    143 
    144     # Add CPU frequency measurements.
    145     frequency_hz = whole_package_utilization.get('frequency_percent')
    146     if frequency_hz is not None:
    147       frequency_sum = 0.0
    148       for freq, percent in frequency_hz.iteritems():
    149         frequency_sum += freq * (percent / 100.0)
    150       results.AddValue(scalar.ScalarValue(
    151           results.current_page, 'cpu_average_frequency_hz', 'Hz',
    152           frequency_sum, important=False))
    153 
    154     # Add CPU c-state residency measurements.
    155     cstate_percent = whole_package_utilization.get('cstate_residency_percent')
    156     if cstate_percent is not None:
    157       for state, percent in cstate_percent.iteritems():
    158         results.AddValue(scalar.ScalarValue(
    159             results.current_page, 'cpu_cstate_%s_residency_percent' % state,
    160             '%', percent, important=False))
    161 
    162     self._results = None
    163 
    164 def _SubtractCpuStats(cpu_stats, start_cpu_stats):
    165   """Computes number of idle wakeups that occurred over measurement period.
    166 
    167   Each of the two cpu_stats arguments is a dict as returned by the
    168   Browser.cpu_stats call.
    169 
    170   Returns:
    171     A dict of process type names (Browser, Renderer, etc.) to idle wakeup count
    172     over the period recorded by the input.
    173   """
    174   cpu_delta = {}
    175   total = 0
    176   for process_type in cpu_stats:
    177     assert process_type in start_cpu_stats, 'Mismatching process types'
    178     # Skip any process_types that are empty.
    179     if (not cpu_stats[process_type]) or (not start_cpu_stats[process_type]):
    180       continue
    181     # Skip if IdleWakeupCount is not present.
    182     if (('IdleWakeupCount' not in cpu_stats[process_type]) or
    183         ('IdleWakeupCount' not in start_cpu_stats[process_type])):
    184       continue
    185 
    186     assert isinstance(cpu_stats[process_type]['IdleWakeupCount'],
    187         process_statistic_timeline_data.IdleWakeupTimelineData)
    188     idle_wakeup_delta = (cpu_stats[process_type]['IdleWakeupCount'] -
    189                         start_cpu_stats[process_type]['IdleWakeupCount'])
    190     cpu_delta[process_type] = idle_wakeup_delta.total_sum()
    191     total = total + cpu_delta[process_type]
    192   cpu_delta['Total'] = total
    193   return cpu_delta
    194