Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 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 window from the output of SurfaceFlinger. | |
| 21 | |
| 22 Args: | |
| 23 adb: the adb coonection to use. | |
| 24 window_package: Package name of the window. | |
| 25 window_activity: Activity name of the window. | |
| 26 """ | |
| 27 def __init__(self, adb, window_package, window_activity): | |
| 28 self._adb = adb | |
| 29 self._window_package = window_package | |
| 30 self._window_activity = window_activity | |
| 31 self._collector_thread = None | |
| 32 self._use_legacy_method = False | |
| 33 self._surface_before = None | |
| 34 self._get_data_event = None | |
| 35 self._data_queue = None | |
| 36 self._stop_event = None | |
| 37 | |
| 38 def Start(self): | |
| 39 assert not self._collector_thread | |
| 40 | |
| 41 if self._ClearSurfaceFlingerLatencyData(): | |
| 42 self._get_data_event = threading.Event() | |
| 43 self._stop_event = threading.Event() | |
| 44 self._data_queue = Queue.Queue() | |
| 45 self._collector_thread = threading.Thread(target=self._CollectorThread) | |
| 46 self._collector_thread.start() | |
| 47 self._adb.StartRecordingLogcat(clear=False) | |
| 48 else: | |
| 49 self._use_legacy_method = True | |
| 50 self._surface_before = self._GetSurfaceStatsLegacy() | |
| 51 | |
| 52 def Stop(self): | |
| 53 if self._collector_thread: | |
| 54 self._stop_event.set() | |
| 55 self._collector_thread.join() | |
| 56 self._collector_thread = None | |
| 57 | |
| 58 def PrintPerfResults(self, trace_tag): | |
| 59 if self._use_legacy_method: | |
| 60 surface_after = self._GetSurfaceStatsLegacy() | |
| 61 td = surface_after['timestamp'] - self._surface_before['timestamp'] | |
| 62 seconds = td.seconds + td.microseconds / 1e6 | |
| 63 frame_count = (surface_after['page_flip_count'] - | |
| 64 self._surface_before['page_flip_count']) | |
| 65 else: | |
| 66 assert self._collector_thread | |
| 67 (seconds, latencies, app_timestamps) = self._GetDataFromThread() | |
| 68 if not seconds or not len(latencies): | |
| 69 logging.warning('Surface stat data is empty') | |
| 70 return | |
| 71 | |
| 72 renderer_timestamps = self._CollectSurfaceTextureTimestamps() | |
|
Sami
2012/10/18 11:35:57
This bit depends on the browser sending us timesta
bulach
2012/10/18 12:33:56
thanks for the clarification sami! it simplifies t
| |
| 73 if not renderer_timestamps: | |
| 74 logging.warning('No renderer timestamp data') | |
| 75 elif app_timestamps: | |
| 76 dropped_frames, repeated_frames, use_count = \ | |
| 77 self._CalculateDroppedAndRepeatedFrames(app_timestamps, | |
| 78 renderer_timestamps) | |
| 79 perf_tests_helper.PrintPerfResult( | |
| 80 'dropped_frames', 'dropped_frames' + trace_tag, | |
| 81 [100.0 * dropped_frames / len(use_count)], 'percent') | |
| 82 perf_tests_helper.PrintPerfResult( | |
| 83 'repeated_frames', 'repeated_frames' + trace_tag, | |
| 84 [100.0 * repeated_frames / len(use_count)], 'percent') | |
| 85 perf_tests_helper.PrintPerfResult( | |
| 86 'frame_use_count', 'frame_use_count' + trace_tag, use_count, '') | |
| 87 | |
| 88 frame_count = len(latencies) | |
| 89 jitter_count = 0 | |
| 90 last_latency = latencies[0] | |
| 91 for latency in latencies[1:]: | |
| 92 if latency > last_latency: | |
| 93 jitter_count = jitter_count + 1 | |
| 94 last_latency = latency | |
| 95 | |
| 96 perf_tests_helper.PrintPerfResult( | |
| 97 'surface_latencies', 'surface_latencies' + trace_tag, latencies, '') | |
| 98 perf_tests_helper.PrintPerfResult( | |
| 99 'peak_jitter', 'peak_jitter' + trace_tag, [max(latencies)], '') | |
| 100 perf_tests_helper.PrintPerfResult( | |
| 101 'jitter_percent', 'jitter_percent' + trace_tag, | |
| 102 [jitter_count * 100.0 / frame_count], 'percent') | |
| 103 | |
| 104 print 'SurfaceMonitorTime: %fsecs' % seconds | |
| 105 perf_tests_helper.PrintPerfResult( | |
| 106 'avg_surface_fps', 'avg_surface_fps' + trace_tag, | |
| 107 [int(round(frame_count / seconds))], 'fps') | |
| 108 | |
| 109 def _CollectSurfaceTextureTimestamps(self): | |
| 110 """Extracts Surface Texture timestamps from logcat messages as generated by | |
| 111 --log-fps. | |
| 112 | |
| 113 Returns: | |
| 114 A list of integer time stamps sorted in ascending order. | |
| 115 """ | |
| 116 record = self._adb.StopRecordingLogcat() | |
| 117 timestamp_lines = self._adb.SearchLogcatRecord(record, | |
| 118 _SURFACE_TEXTURE_TIMESTAMPS_MESSAGE) | |
| 119 timestamps = [] | |
| 120 for line in timestamp_lines: | |
| 121 for match in re.findall(_SURFACE_TEXTURE_TIMESTAMP_RE, line['message']): | |
| 122 timestamp = long(match) | |
| 123 # TODO(skyostil): Figure out why the first two timestamps are zero. | |
| 124 if timestamp > 0: | |
| 125 timestamps.append(timestamp) | |
| 126 timestamps.sort() | |
| 127 return timestamps | |
| 128 | |
| 129 @staticmethod | |
| 130 def _CalculateDroppedAndRepeatedFrames(app_timestamps, renderer_timestamps): | |
| 131 """Calculate the number of dropped and repeated renderer frames. This is | |
| 132 done by calculating which renderer produced frame is used in each | |
| 133 application produced frame. To reduce false positives, only the | |
| 134 intersection of the timestamp ranges is examined. | |
| 135 | |
| 136 Args: | |
| 137 app_timestamps: A sorted list of application frame timestamps. | |
| 138 renderer_timestamps: A sorted list of renderer frame timestamps. | |
| 139 | |
| 140 Returns: | |
| 141 A tuple containing dropped and repeated frame counts and list of integer | |
| 142 use counts for every renderer frame in the examined range. | |
| 143 """ | |
| 144 # Reduce the timestamps to the intersection of both sequences. | |
| 145 min_app, max_app = app_timestamps[0], app_timestamps[-1] | |
| 146 min_renderer, max_renderer = renderer_timestamps[0], renderer_timestamps[-1] | |
| 147 min_time = max(min_app, min_renderer) | |
| 148 max_time = min(max_app, max_renderer) | |
| 149 def FindFirstIndexOver(timestamps, min_time): | |
| 150 for i in range(len(timestamps)): | |
| 151 if timestamps[i] >= min_time: | |
| 152 return i | |
| 153 def FindLastIndexUnder(timestamps, max_time): | |
| 154 for i in range(len(timestamps) - 1, 0, -1): | |
| 155 if timestamps[i] <= max_time: | |
| 156 return i | |
| 157 def Intersect(timestamps, min_time, max_time): | |
| 158 min_index = FindFirstIndexOver(timestamps, min_time) | |
| 159 max_index = FindLastIndexUnder(timestamps, max_time) | |
| 160 if min_index is None or max_index is None: | |
| 161 return [] | |
| 162 return timestamps[min_index:max_index + 1] | |
| 163 app_timestamps = Intersect(app_timestamps, min_time, max_time) | |
| 164 renderer_timestamps = Intersect(renderer_timestamps, min_time, max_time) | |
| 165 | |
| 166 # Discard the last renderer frame because any app frame consuming it will be | |
| 167 # outside the intersected range. | |
| 168 renderer_timestamps = renderer_timestamps[:-1] | |
| 169 | |
| 170 # If there was no common time interval, bail out. | |
| 171 if not app_timestamps or not renderer_timestamps: | |
| 172 return (0, 0, [0]) | |
| 173 | |
| 174 renderer_frame_use_count = dict((t, 0) for t in renderer_timestamps) | |
| 175 for app_time in app_timestamps: | |
| 176 # Find out which renderer frame was used to draw this app frame. | |
| 177 for renderer_time in reversed(renderer_timestamps): | |
| 178 if renderer_time < app_time: | |
| 179 renderer_frame_use_count[renderer_time] += 1 | |
| 180 break | |
| 181 | |
| 182 # Calculate statistics on dropped and repeated frames. | |
| 183 dropped_frames = 0 | |
| 184 repeated_frames = 0 | |
| 185 for renderer_time, use_count in renderer_frame_use_count.items(): | |
| 186 if use_count == 0: | |
| 187 dropped_frames += 1 | |
| 188 elif use_count > 1: | |
| 189 repeated_frames += use_count - 1 | |
| 190 return (dropped_frames, repeated_frames, renderer_frame_use_count.values()) | |
| 191 | |
| 192 def _CollectorThread(self): | |
| 193 last_timestamp = 0 | |
| 194 first_timestamp = 0 | |
| 195 latencies = [] | |
| 196 timestamps = [] | |
| 197 | |
| 198 while not self._stop_event.is_set(): | |
| 199 self._get_data_event.wait(1) | |
| 200 try: | |
| 201 (t, last_timestamp) = self._GetSurfaceFlingerLatencyData(last_timestamp, | |
| 202 latencies, | |
| 203 timestamps) | |
| 204 if not first_timestamp: | |
| 205 first_timestamp = t | |
| 206 | |
| 207 if self._get_data_event.is_set(): | |
| 208 self._get_data_event.clear() | |
| 209 self._data_queue.put(((last_timestamp - first_timestamp) / 1e9, | |
| 210 latencies, timestamps)) | |
| 211 latencies = [] | |
| 212 timestamps = [] | |
| 213 first_timestamp = 0 | |
| 214 except Exception as e: | |
| 215 # On any error, before aborting, put the exception into _data_queue to | |
| 216 # prevent the main thread from waiting at _data_queue.get() infinitely. | |
| 217 self._data_queue.put(e) | |
| 218 raise | |
| 219 | |
| 220 def _GetDataFromThread(self): | |
| 221 self._get_data_event.set() | |
| 222 ret = self._data_queue.get() | |
| 223 if isinstance(ret, Exception): | |
| 224 raise ret | |
| 225 return ret | |
| 226 | |
| 227 def _ClearSurfaceFlingerLatencyData(self): | |
| 228 """Clears the SurfaceFlinger latency data. | |
| 229 | |
| 230 Returns: | |
| 231 True if SurfaceFlinger latency is supported by the device, otherwise | |
| 232 False. | |
| 233 """ | |
| 234 # The command returns nothing if it is supported, otherwise returns many | |
| 235 # lines of result just like 'dumpsys SurfaceFlinger'. | |
| 236 results = self._adb.RunShellCommand( | |
| 237 'dumpsys SurfaceFlinger --latency-clear %s/%s' % | |
| 238 (self._window_package, self._window_activity)) | |
| 239 return not len(results) | |
| 240 | |
| 241 def _GetSurfaceFlingerLatencyData(self, previous_timestamp, latencies, | |
| 242 timestamps): | |
| 243 """Returns collected SurfaceFlinger latency data. | |
| 244 | |
| 245 Args: | |
| 246 previous_timestamp: The timestamp returned from the previous call or 0. | |
| 247 Only data after this timestamp will be returned. | |
| 248 latencies: A list to receive latency data. The latencies are integers | |
| 249 each of which is the number of refresh periods of each frame. | |
| 250 timestamps: A list to receive timestamp data. The timestamps indicate | |
| 251 when the application started drawing each frame. | |
| 252 | |
| 253 Returns: | |
| 254 A tuple containing: | |
| 255 - The timestamp of the beginning of the first frame (ns), | |
| 256 - The timestamp of the end of the last frame (ns). | |
| 257 | |
| 258 Raises: | |
| 259 Exception if failed to run the SurfaceFlinger command or SurfaceFlinger | |
| 260 returned invalid result. | |
| 261 """ | |
| 262 # adb shell dumpsys SurfaceFlinger --latency <window name> | |
| 263 # prints some information about the last 128 frames displayed in | |
| 264 # that window. | |
| 265 # The data returned looks like this: | |
| 266 # 16954612 | |
| 267 # 7657467895508 7657482691352 7657493499756 | |
| 268 # 7657484466553 7657499645964 7657511077881 | |
| 269 # 7657500793457 7657516600576 7657527404785 | |
| 270 # (...) | |
| 271 # | |
| 272 # The first line is the refresh period (here 16.95 ms), it is followed | |
| 273 # by 128 lines w/ 3 timestamps in nanosecond each: | |
| 274 # A) when the app started to draw | |
| 275 # B) the vsync immediately preceding SF submitting the frame to the h/w | |
| 276 # C) timestamp immediately after SF submitted that frame to the h/w | |
| 277 # | |
| 278 # The difference between the 1st and 3rd timestamp is the frame-latency. | |
| 279 # An interesting data is when the frame latency crosses a refresh period | |
| 280 # boundary, this can be calculated this way: | |
| 281 # | |
| 282 # ceil((C - A) / refresh-period) | |
| 283 # | |
| 284 # (each time the number above changes, we have a "jank"). | |
| 285 # If this happens a lot during an animation, the animation appears | |
| 286 # janky, even if it runs at 60 fps in average. | |
| 287 results = self._adb.RunShellCommand( | |
| 288 'dumpsys SurfaceFlinger --latency %s/%s' % | |
| 289 (self._window_package, self._window_activity), log_result=True) | |
| 290 assert len(results) | |
| 291 | |
| 292 refresh_period = int(results[0]) | |
| 293 last_timestamp = previous_timestamp | |
| 294 first_timestamp = 0 | |
| 295 for line in results[1:]: | |
| 296 fields = line.split() | |
| 297 if len(fields) == 3: | |
| 298 timestamp = long(fields[0]) | |
| 299 last_timestamp = long(fields[2]) | |
| 300 if (timestamp > previous_timestamp): | |
| 301 if not first_timestamp: | |
| 302 first_timestamp = timestamp | |
| 303 # This is integral equivalent of ceil((C-A) / refresh-period) | |
| 304 latency_ns = int(last_timestamp - timestamp) | |
| 305 latencies.append((latency_ns + refresh_period - 1) / refresh_period) | |
| 306 timestamps.append(timestamp) | |
| 307 return (first_timestamp, last_timestamp) | |
| 308 | |
| 309 def _GetSurfaceStatsLegacy(self): | |
| 310 """Legacy method (before JellyBean), returns the current Surface index | |
| 311 and timestamp. | |
| 312 | |
| 313 Calculate FPS by measuring the difference of Surface index returned by | |
| 314 SurfaceFlinger in a period of time. | |
| 315 | |
| 316 Returns: | |
| 317 Dict of {page_flip_count (or 0 if there was an error), timestamp}. | |
| 318 """ | |
| 319 results = self._adb.RunShellCommand('service call SurfaceFlinger 1013') | |
| 320 assert len(results) == 1 | |
| 321 match = re.search('^Result: Parcel\((\w+)', results[0]) | |
| 322 cur_surface = 0 | |
| 323 if match: | |
| 324 try: | |
| 325 cur_surface = int(match.group(1), 16) | |
| 326 except Exception: | |
| 327 logging.error('Failed to parse current surface from ' + match.group(1)) | |
| 328 else: | |
| 329 logging.warning('Failed to call SurfaceFlinger surface ' + results[0]) | |
| 330 return { | |
| 331 'page_flip_count': cur_surface, | |
| 332 'timestamp': datetime.datetime.now(), | |
| 333 } | |
| OLD | NEW |