Index: build/android/pylib/surface_stats_collector.py |
diff --git a/build/android/pylib/surface_stats_collector.py b/build/android/pylib/surface_stats_collector.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9c0cb7e32c6c1adcca8047685cee040fb1d43721 |
--- /dev/null |
+++ b/build/android/pylib/surface_stats_collector.py |
@@ -0,0 +1,229 @@ |
+# Copyright (c) 2012 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 Queue |
+import datetime |
+import logging |
+import re |
+import threading |
+ |
+from pylib import perf_tests_helper |
+ |
+ |
+# Log marker containing SurfaceTexture timestamps. |
+_SURFACE_TEXTURE_TIMESTAMPS_MESSAGE = 'SurfaceTexture update timestamps' |
+_SURFACE_TEXTURE_TIMESTAMP_RE = '\d+' |
+ |
+ |
+class SurfaceStatsCollector(object): |
+ """Collects surface stats for a window from the output of SurfaceFlinger. |
+ |
+ Args: |
+ adb: the adb coonection to use. |
+ window_package: Package name of the window. |
+ window_activity: Activity name of the window. |
+ """ |
+ def __init__(self, adb, window_package, window_activity, trace_tag): |
+ self._adb = adb |
+ self._window_package = window_package |
+ self._window_activity = window_activity |
+ self._trace_tag = trace_tag |
+ self._collector_thread = None |
+ self._use_legacy_method = False |
+ self._surface_before = None |
+ self._get_data_event = None |
+ self._data_queue = None |
+ self._stop_event = None |
+ |
+ def __enter__(self): |
+ assert not self._collector_thread |
+ |
+ if self._ClearSurfaceFlingerLatencyData(): |
+ self._get_data_event = threading.Event() |
+ self._stop_event = threading.Event() |
+ self._data_queue = Queue.Queue() |
+ self._collector_thread = threading.Thread(target=self._CollectorThread) |
+ self._collector_thread.start() |
+ else: |
+ self._use_legacy_method = True |
+ self._surface_before = self._GetSurfaceStatsLegacy() |
+ |
+ def __exit__(self, *args): |
+ self._PrintPerfResults() |
+ if self._collector_thread: |
+ self._stop_event.set() |
+ self._collector_thread.join() |
+ self._collector_thread = None |
+ |
+ def _PrintPerfResults(self): |
+ if self._use_legacy_method: |
+ surface_after = self._GetSurfaceStatsLegacy() |
+ td = surface_after['timestamp'] - self._surface_before['timestamp'] |
+ seconds = td.seconds + td.microseconds / 1e6 |
+ frame_count = (surface_after['page_flip_count'] - |
+ self._surface_before['page_flip_count']) |
+ else: |
+ assert self._collector_thread |
+ (seconds, latencies) = self._GetDataFromThread() |
+ if not seconds or not len(latencies): |
+ logging.warning('Surface stat data is empty') |
+ return |
+ |
+ frame_count = len(latencies) |
+ jitter_count = 0 |
+ last_latency = latencies[0] |
+ for latency in latencies[1:]: |
+ if latency > last_latency: |
+ jitter_count = jitter_count + 1 |
+ last_latency = latency |
+ |
+ perf_tests_helper.PrintPerfResult( |
+ 'surface_latencies', 'surface_latencies' + self._trace_tag, |
+ latencies, '') |
+ perf_tests_helper.PrintPerfResult( |
+ 'peak_jitter', 'peak_jitter' + self._trace_tag, [max(latencies)], '') |
+ perf_tests_helper.PrintPerfResult( |
+ 'jitter_percent', 'jitter_percent' + self._trace_tag, |
+ [jitter_count * 100.0 / frame_count], 'percent') |
+ |
+ print 'SurfaceMonitorTime: %fsecs' % seconds |
+ perf_tests_helper.PrintPerfResult( |
+ 'avg_surface_fps', 'avg_surface_fps' + self._trace_tag, |
+ [int(round(frame_count / seconds))], 'fps') |
+ |
+ def _CollectorThread(self): |
+ last_timestamp = 0 |
+ first_timestamp = 0 |
+ latencies = [] |
+ |
+ while not self._stop_event.is_set(): |
+ self._get_data_event.wait(1) |
+ try: |
+ (t, last_timestamp) = self._GetSurfaceFlingerLatencyData(last_timestamp, |
+ latencies) |
+ if not first_timestamp: |
+ first_timestamp = t |
+ |
+ if self._get_data_event.is_set(): |
+ self._get_data_event.clear() |
+ self._data_queue.put(((last_timestamp - first_timestamp) / 1e9, |
+ latencies)) |
+ latencies = [] |
+ first_timestamp = 0 |
+ except Exception as e: |
+ # On any error, before aborting, put the exception into _data_queue to |
+ # prevent the main thread from waiting at _data_queue.get() infinitely. |
+ self._data_queue.put(e) |
+ raise |
+ |
+ def _GetDataFromThread(self): |
+ self._get_data_event.set() |
+ ret = self._data_queue.get() |
+ if isinstance(ret, Exception): |
+ raise ret |
+ return ret |
+ |
+ def _ClearSurfaceFlingerLatencyData(self): |
+ """Clears the SurfaceFlinger latency data. |
+ |
+ Returns: |
+ True if SurfaceFlinger latency is supported by the device, otherwise |
+ False. |
+ """ |
+ # The command returns nothing if it is supported, otherwise returns many |
+ # lines of result just like 'dumpsys SurfaceFlinger'. |
+ results = self._adb.RunShellCommand( |
+ 'dumpsys SurfaceFlinger --latency-clear %s/%s' % |
+ (self._window_package, self._window_activity)) |
+ return not len(results) |
+ |
+ def _GetSurfaceFlingerLatencyData(self, previous_timestamp, latencies): |
+ """Returns collected SurfaceFlinger latency data. |
+ |
+ Args: |
+ previous_timestamp: The timestamp returned from the previous call or 0. |
+ Only data after this timestamp will be returned. |
+ latencies: A list to receive latency data. The latencies are integers |
+ each of which is the number of refresh periods of each frame. |
+ |
+ Returns: |
+ A tuple containing: |
+ - The timestamp of the beginning of the first frame (ns), |
+ - The timestamp of the end of the last frame (ns). |
+ |
+ Raises: |
+ Exception if failed to run the SurfaceFlinger command or SurfaceFlinger |
+ returned invalid result. |
+ """ |
+ # adb shell dumpsys SurfaceFlinger --latency <window name> |
+ # prints some information about the last 128 frames displayed in |
+ # that window. |
+ # The data returned looks like this: |
+ # 16954612 |
+ # 7657467895508 7657482691352 7657493499756 |
+ # 7657484466553 7657499645964 7657511077881 |
+ # 7657500793457 7657516600576 7657527404785 |
+ # (...) |
+ # |
+ # The first line is the refresh period (here 16.95 ms), it is followed |
+ # by 128 lines w/ 3 timestamps in nanosecond each: |
+ # A) when the app started to draw |
+ # B) the vsync immediately preceding SF submitting the frame to the h/w |
+ # C) timestamp immediately after SF submitted that frame to the h/w |
+ # |
+ # The difference between the 1st and 3rd timestamp is the frame-latency. |
+ # An interesting data is when the frame latency crosses a refresh period |
+ # boundary, this can be calculated this way: |
+ # |
+ # ceil((C - A) / refresh-period) |
+ # |
+ # (each time the number above changes, we have a "jank"). |
+ # If this happens a lot during an animation, the animation appears |
+ # janky, even if it runs at 60 fps in average. |
+ results = self._adb.RunShellCommand( |
+ 'dumpsys SurfaceFlinger --latency %s/%s' % |
+ (self._window_package, self._window_activity), log_result=True) |
+ assert len(results) |
+ |
+ refresh_period = int(results[0]) |
+ last_timestamp = previous_timestamp |
+ first_timestamp = 0 |
+ for line in results[1:]: |
+ fields = line.split() |
+ if len(fields) == 3: |
+ timestamp = long(fields[0]) |
+ last_timestamp = long(fields[2]) |
+ if (timestamp > previous_timestamp): |
+ if not first_timestamp: |
+ first_timestamp = timestamp |
+ # This is integral equivalent of ceil((C-A) / refresh-period) |
+ latency_ns = int(last_timestamp - timestamp) |
+ latencies.append((latency_ns + refresh_period - 1) / refresh_period) |
+ return (first_timestamp, last_timestamp) |
+ |
+ def _GetSurfaceStatsLegacy(self): |
+ """Legacy method (before JellyBean), returns the current Surface index |
+ and timestamp. |
+ |
+ Calculate FPS by measuring the difference of Surface index returned by |
+ SurfaceFlinger in a period of time. |
+ |
+ Returns: |
+ Dict of {page_flip_count (or 0 if there was an error), timestamp}. |
+ """ |
+ results = self._adb.RunShellCommand('service call SurfaceFlinger 1013') |
+ assert len(results) == 1 |
+ match = re.search('^Result: Parcel\((\w+)', results[0]) |
+ cur_surface = 0 |
+ if match: |
+ try: |
+ cur_surface = int(match.group(1), 16) |
+ except Exception: |
+ logging.error('Failed to parse current surface from ' + match.group(1)) |
+ else: |
+ logging.warning('Failed to call SurfaceFlinger surface ' + results[0]) |
+ return { |
+ 'page_flip_count': cur_surface, |
+ 'timestamp': datetime.datetime.now(), |
+ } |