Home | History | Annotate | Download | only in pylib
      1 # Copyright (c) 2012 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 Queue
      6 import datetime
      7 import logging
      8 import re
      9 import threading
     10 
     11 from pylib import perf_tests_helper
     12 
     13 
     14 # Log marker containing SurfaceTexture timestamps.
     15 _SURFACE_TEXTURE_TIMESTAMPS_MESSAGE = 'SurfaceTexture update timestamps'
     16 _SURFACE_TEXTURE_TIMESTAMP_RE = '\d+'
     17 
     18 
     19 class SurfaceStatsCollector(object):
     20   """Collects surface stats for a SurfaceView from the output of SurfaceFlinger.
     21 
     22   Args:
     23     adb: the adb connection to use.
     24   """
     25   class Result(object):
     26     def __init__(self, name, value, unit):
     27       self.name = name
     28       self.value = value
     29       self.unit = unit
     30 
     31   def __init__(self, adb):
     32     self._adb = adb
     33     self._collector_thread = None
     34     self._use_legacy_method = False
     35     self._surface_before = None
     36     self._get_data_event = None
     37     self._data_queue = None
     38     self._stop_event = None
     39     self._results = []
     40     self._warn_about_empty_data = True
     41 
     42   def DisableWarningAboutEmptyData(self):
     43     self._warn_about_empty_data = False
     44 
     45   def Start(self):
     46     assert not self._collector_thread
     47 
     48     if self._ClearSurfaceFlingerLatencyData():
     49       self._get_data_event = threading.Event()
     50       self._stop_event = threading.Event()
     51       self._data_queue = Queue.Queue()
     52       self._collector_thread = threading.Thread(target=self._CollectorThread)
     53       self._collector_thread.start()
     54     else:
     55       self._use_legacy_method = True
     56       self._surface_before = self._GetSurfaceStatsLegacy()
     57 
     58   def Stop(self):
     59     self._StorePerfResults()
     60     if self._collector_thread:
     61       self._stop_event.set()
     62       self._collector_thread.join()
     63       self._collector_thread = None
     64 
     65   def SampleResults(self):
     66     self._StorePerfResults()
     67     results = self.GetResults()
     68     self._results = []
     69     return results
     70 
     71   def GetResults(self):
     72     return self._results or self._GetEmptyResults()
     73 
     74   def _GetEmptyResults(self):
     75     return [
     76         SurfaceStatsCollector.Result('refresh_period', None, 'seconds'),
     77         SurfaceStatsCollector.Result('jank_count', None, 'janks'),
     78         SurfaceStatsCollector.Result('max_frame_delay', None, 'vsyncs'),
     79         SurfaceStatsCollector.Result('frame_lengths', None, 'vsyncs'),
     80         SurfaceStatsCollector.Result('avg_surface_fps', None, 'fps')
     81     ]
     82 
     83   @staticmethod
     84   def _GetNormalizedDeltas(data, refresh_period):
     85     deltas = [t2 - t1 for t1, t2 in zip(data, data[1:])]
     86     return (deltas, [delta / refresh_period for delta in deltas])
     87 
     88   @staticmethod
     89   def _CalculateResults(refresh_period, timestamps, result_suffix):
     90     """Returns a list of SurfaceStatsCollector.Result."""
     91     frame_count = len(timestamps)
     92     seconds = timestamps[-1] - timestamps[0]
     93 
     94     frame_lengths, normalized_frame_lengths = \
     95         SurfaceStatsCollector._GetNormalizedDeltas(timestamps, refresh_period)
     96     length_changes, normalized_changes = \
     97         SurfaceStatsCollector._GetNormalizedDeltas(
     98             frame_lengths, refresh_period)
     99     jankiness = [max(0, round(change)) for change in normalized_changes]
    100     pause_threshold = 20
    101     jank_count = sum(1 for change in jankiness
    102                      if change > 0 and change < pause_threshold)
    103     return [
    104         SurfaceStatsCollector.Result(
    105             'avg_surface_fps' + result_suffix,
    106             int(round(frame_count / seconds)), 'fps'),
    107         SurfaceStatsCollector.Result(
    108             'jank_count' + result_suffix, jank_count, 'janks'),
    109         SurfaceStatsCollector.Result(
    110             'max_frame_delay' + result_suffix,
    111             round(max(normalized_frame_lengths)),
    112             'vsyncs'),
    113         SurfaceStatsCollector.Result(
    114             'frame_lengths' + result_suffix, normalized_frame_lengths,
    115             'vsyncs'),
    116     ]
    117 
    118   @staticmethod
    119   def _CalculateBuckets(refresh_period, timestamps):
    120     results = []
    121     for pct in [0.99, 0.5]:
    122       sliced = timestamps[min(int(-pct * len(timestamps)), -3) : ]
    123       results += SurfaceStatsCollector._CalculateResults(
    124           refresh_period, sliced, '_' + str(int(pct * 100)))
    125     return results
    126 
    127   def _StorePerfResults(self):
    128     if self._use_legacy_method:
    129       surface_after = self._GetSurfaceStatsLegacy()
    130       td = surface_after['timestamp'] - self._surface_before['timestamp']
    131       seconds = td.seconds + td.microseconds / 1e6
    132       frame_count = (surface_after['page_flip_count'] -
    133                      self._surface_before['page_flip_count'])
    134       self._results.append(SurfaceStatsCollector.Result(
    135           'avg_surface_fps', int(round(frame_count / seconds)), 'fps'))
    136       return
    137 
    138     # Non-legacy method.
    139     assert self._collector_thread
    140     (refresh_period, timestamps) = self._GetDataFromThread()
    141     if not refresh_period or not len(timestamps) >= 3:
    142       if self._warn_about_empty_data:
    143         logging.warning('Surface stat data is empty')
    144       return
    145     self._results.append(SurfaceStatsCollector.Result(
    146         'refresh_period', refresh_period, 'seconds'))
    147     self._results += self._CalculateResults(refresh_period, timestamps, '')
    148     self._results += self._CalculateBuckets(refresh_period, timestamps)
    149 
    150   def _CollectorThread(self):
    151     last_timestamp = 0
    152     timestamps = []
    153     retries = 0
    154 
    155     while not self._stop_event.is_set():
    156       self._get_data_event.wait(1)
    157       try:
    158         refresh_period, new_timestamps = self._GetSurfaceFlingerFrameData()
    159         if refresh_period is None or timestamps is None:
    160           retries += 1
    161           if retries < 3:
    162             continue
    163           if last_timestamp:
    164             # Some data has already been collected, but either the app
    165             # was closed or there's no new data. Signal the main thread and
    166             # wait.
    167             self._data_queue.put((None, None))
    168             self._stop_event.wait()
    169             break
    170           raise Exception('Unable to get surface flinger latency data')
    171 
    172         timestamps += [timestamp for timestamp in new_timestamps
    173                        if timestamp > last_timestamp]
    174         if len(timestamps):
    175           last_timestamp = timestamps[-1]
    176 
    177         if self._get_data_event.is_set():
    178           self._get_data_event.clear()
    179           self._data_queue.put((refresh_period, timestamps))
    180           timestamps = []
    181       except Exception as e:
    182         # On any error, before aborting, put the exception into _data_queue to
    183         # prevent the main thread from waiting at _data_queue.get() infinitely.
    184         self._data_queue.put(e)
    185         raise
    186 
    187   def _GetDataFromThread(self):
    188     self._get_data_event.set()
    189     ret = self._data_queue.get()
    190     if isinstance(ret, Exception):
    191       raise ret
    192     return ret
    193 
    194   def _ClearSurfaceFlingerLatencyData(self):
    195     """Clears the SurfaceFlinger latency data.
    196 
    197     Returns:
    198       True if SurfaceFlinger latency is supported by the device, otherwise
    199       False.
    200     """
    201     # The command returns nothing if it is supported, otherwise returns many
    202     # lines of result just like 'dumpsys SurfaceFlinger'.
    203     results = self._adb.RunShellCommand(
    204         'dumpsys SurfaceFlinger --latency-clear SurfaceView')
    205     return not len(results)
    206 
    207   def _GetSurfaceFlingerFrameData(self):
    208     """Returns collected SurfaceFlinger frame timing data.
    209 
    210     Returns:
    211       A tuple containing:
    212       - The display's nominal refresh period in seconds.
    213       - A list of timestamps signifying frame presentation times in seconds.
    214       The return value may be (None, None) if there was no data collected (for
    215       example, if the app was closed before the collector thread has finished).
    216     """
    217     # adb shell dumpsys SurfaceFlinger --latency <window name>
    218     # prints some information about the last 128 frames displayed in
    219     # that window.
    220     # The data returned looks like this:
    221     # 16954612
    222     # 7657467895508   7657482691352   7657493499756
    223     # 7657484466553   7657499645964   7657511077881
    224     # 7657500793457   7657516600576   7657527404785
    225     # (...)
    226     #
    227     # The first line is the refresh period (here 16.95 ms), it is followed
    228     # by 128 lines w/ 3 timestamps in nanosecond each:
    229     # A) when the app started to draw
    230     # B) the vsync immediately preceding SF submitting the frame to the h/w
    231     # C) timestamp immediately after SF submitted that frame to the h/w
    232     #
    233     # The difference between the 1st and 3rd timestamp is the frame-latency.
    234     # An interesting data is when the frame latency crosses a refresh period
    235     # boundary, this can be calculated this way:
    236     #
    237     # ceil((C - A) / refresh-period)
    238     #
    239     # (each time the number above changes, we have a "jank").
    240     # If this happens a lot during an animation, the animation appears
    241     # janky, even if it runs at 60 fps in average.
    242     #
    243     # We use the special "SurfaceView" window name because the statistics for
    244     # the activity's main window are not updated when the main web content is
    245     # composited into a SurfaceView.
    246     results = self._adb.RunShellCommand(
    247         'dumpsys SurfaceFlinger --latency SurfaceView',
    248         log_result=logging.getLogger().isEnabledFor(logging.DEBUG))
    249     if not len(results):
    250       return (None, None)
    251 
    252     timestamps = []
    253     nanoseconds_per_second = 1e9
    254     refresh_period = long(results[0]) / nanoseconds_per_second
    255 
    256     # If a fence associated with a frame is still pending when we query the
    257     # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX.
    258     # Since we only care about completed frames, we will ignore any timestamps
    259     # with this value.
    260     pending_fence_timestamp = (1 << 63) - 1
    261 
    262     for line in results[1:]:
    263       fields = line.split()
    264       if len(fields) != 3:
    265         continue
    266       timestamp = long(fields[1])
    267       if timestamp == pending_fence_timestamp:
    268         continue
    269       timestamp /= nanoseconds_per_second
    270       timestamps.append(timestamp)
    271 
    272     return (refresh_period, timestamps)
    273 
    274   def _GetSurfaceStatsLegacy(self):
    275     """Legacy method (before JellyBean), returns the current Surface index
    276        and timestamp.
    277 
    278     Calculate FPS by measuring the difference of Surface index returned by
    279     SurfaceFlinger in a period of time.
    280 
    281     Returns:
    282       Dict of {page_flip_count (or 0 if there was an error), timestamp}.
    283     """
    284     results = self._adb.RunShellCommand('service call SurfaceFlinger 1013')
    285     assert len(results) == 1
    286     match = re.search('^Result: Parcel\((\w+)', results[0])
    287     cur_surface = 0
    288     if match:
    289       try:
    290         cur_surface = int(match.group(1), 16)
    291       except Exception:
    292         logging.error('Failed to parse current surface from ' + match.group(1))
    293     else:
    294       logging.warning('Failed to call SurfaceFlinger surface ' + results[0])
    295     return {
    296         'page_flip_count': cur_surface,
    297         'timestamp': datetime.datetime.now(),
    298     }
    299