surface_stats_collector.py revision a0e5c0de428e9dea6d07dd57c5594fb1f1c17c20
1# Copyright 2013 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 5import Queue 6import threading 7 8 9# Log marker containing SurfaceTexture timestamps. 10_SURFACE_TEXTURE_TIMESTAMPS_MESSAGE = 'SurfaceTexture update timestamps' 11_SURFACE_TEXTURE_TIMESTAMP_RE = r'\d+' 12 13 14class SurfaceStatsCollector(object): 15 """Collects surface stats for a SurfaceView from the output of SurfaceFlinger. 16 17 Args: 18 device: A DeviceUtils instance. 19 """ 20 21 def __init__(self, device): 22 self._device = device 23 self._collector_thread = None 24 self._surface_before = None 25 self._get_data_event = None 26 self._data_queue = None 27 self._stop_event = None 28 self._warn_about_empty_data = True 29 30 def DisableWarningAboutEmptyData(self): 31 self._warn_about_empty_data = False 32 33 def Start(self): 34 assert not self._collector_thread 35 36 if self._ClearSurfaceFlingerLatencyData(): 37 self._get_data_event = threading.Event() 38 self._stop_event = threading.Event() 39 self._data_queue = Queue.Queue() 40 self._collector_thread = threading.Thread(target=self._CollectorThread) 41 self._collector_thread.start() 42 else: 43 raise Exception('SurfaceFlinger not supported on this device.') 44 45 def Stop(self): 46 assert self._collector_thread 47 (refresh_period, timestamps) = self._GetDataFromThread() 48 if self._collector_thread: 49 self._stop_event.set() 50 self._collector_thread.join() 51 self._collector_thread = None 52 return (refresh_period, timestamps) 53 54 def _CollectorThread(self): 55 last_timestamp = 0 56 timestamps = [] 57 retries = 0 58 59 while not self._stop_event.is_set(): 60 self._get_data_event.wait(1) 61 try: 62 refresh_period, new_timestamps = self._GetSurfaceFlingerFrameData() 63 if refresh_period is None or timestamps is None: 64 retries += 1 65 if retries < 3: 66 continue 67 if last_timestamp: 68 # Some data has already been collected, but either the app 69 # was closed or there's no new data. Signal the main thread and 70 # wait. 71 self._data_queue.put((None, None)) 72 self._stop_event.wait() 73 break 74 raise Exception('Unable to get surface flinger latency data') 75 76 timestamps += [timestamp for timestamp in new_timestamps 77 if timestamp > last_timestamp] 78 if len(timestamps): 79 last_timestamp = timestamps[-1] 80 81 if self._get_data_event.is_set(): 82 self._get_data_event.clear() 83 self._data_queue.put((refresh_period, timestamps)) 84 timestamps = [] 85 except Exception as e: 86 # On any error, before aborting, put the exception into _data_queue to 87 # prevent the main thread from waiting at _data_queue.get() infinitely. 88 self._data_queue.put(e) 89 raise 90 91 def _GetDataFromThread(self): 92 self._get_data_event.set() 93 ret = self._data_queue.get() 94 if isinstance(ret, Exception): 95 raise ret 96 return ret 97 98 def _ClearSurfaceFlingerLatencyData(self): 99 """Clears the SurfaceFlinger latency data. 100 101 Returns: 102 True if SurfaceFlinger latency is supported by the device, otherwise 103 False. 104 """ 105 # The command returns nothing if it is supported, otherwise returns many 106 # lines of result just like 'dumpsys SurfaceFlinger'. 107 results = self._device.RunShellCommand( 108 ['dumpsys', 'SurfaceFlinger', '--latency-clear', 'SurfaceView'], 109 check_return=True) 110 return not len(results) 111 112 def GetSurfaceFlingerPid(self): 113 pids_dict = self._device.GetPids('surfaceflinger') 114 if not pids_dict: 115 raise Exception('Unable to get surface flinger process id') 116 # TODO(cataput:#3378): Do more strict checks in GetPids when possible. 117 # For now it just returns the first pid found of some matching process. 118 return pids_dict.popitem()[1][0] 119 120 def _GetSurfaceFlingerFrameData(self): 121 """Returns collected SurfaceFlinger frame timing data. 122 123 Returns: 124 A tuple containing: 125 - The display's nominal refresh period in milliseconds. 126 - A list of timestamps signifying frame presentation times in 127 milliseconds. 128 The return value may be (None, None) if there was no data collected (for 129 example, if the app was closed before the collector thread has finished). 130 """ 131 # adb shell dumpsys SurfaceFlinger --latency <window name> 132 # prints some information about the last 128 frames displayed in 133 # that window. 134 # The data returned looks like this: 135 # 16954612 136 # 7657467895508 7657482691352 7657493499756 137 # 7657484466553 7657499645964 7657511077881 138 # 7657500793457 7657516600576 7657527404785 139 # (...) 140 # 141 # The first line is the refresh period (here 16.95 ms), it is followed 142 # by 128 lines w/ 3 timestamps in nanosecond each: 143 # A) when the app started to draw 144 # B) the vsync immediately preceding SF submitting the frame to the h/w 145 # C) timestamp immediately after SF submitted that frame to the h/w 146 # 147 # The difference between the 1st and 3rd timestamp is the frame-latency. 148 # An interesting data is when the frame latency crosses a refresh period 149 # boundary, this can be calculated this way: 150 # 151 # ceil((C - A) / refresh-period) 152 # 153 # (each time the number above changes, we have a "jank"). 154 # If this happens a lot during an animation, the animation appears 155 # janky, even if it runs at 60 fps in average. 156 # 157 # We use the special "SurfaceView" window name because the statistics for 158 # the activity's main window are not updated when the main web content is 159 # composited into a SurfaceView. 160 results = self._device.RunShellCommand( 161 ['dumpsys', 'SurfaceFlinger', '--latency', 'SurfaceView'], 162 check_return=True) 163 if not len(results): 164 return (None, None) 165 166 timestamps = [] 167 nanoseconds_per_millisecond = 1e6 168 refresh_period = long(results[0]) / nanoseconds_per_millisecond 169 170 # If a fence associated with a frame is still pending when we query the 171 # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX. 172 # Since we only care about completed frames, we will ignore any timestamps 173 # with this value. 174 pending_fence_timestamp = (1 << 63) - 1 175 176 for line in results[1:]: 177 fields = line.split() 178 if len(fields) != 3: 179 continue 180 timestamp = long(fields[1]) 181 if timestamp == pending_fence_timestamp: 182 continue 183 timestamp /= nanoseconds_per_millisecond 184 timestamps.append(timestamp) 185 186 return (refresh_period, timestamps) 187