Index: build/android/pylib/perf/surface_stats_collector.py |
diff --git a/build/android/pylib/perf/surface_stats_collector.py b/build/android/pylib/perf/surface_stats_collector.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c7e7527a1e25c2ef663f5f80c09f6f80696bbba7 |
--- /dev/null |
+++ b/build/android/pylib/perf/surface_stats_collector.py |
@@ -0,0 +1,191 @@ |
+# Copyright 2013 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 android_commands |
+from pylib.device import device_utils |
+ |
+ |
+# Log marker containing SurfaceTexture timestamps. |
+_SURFACE_TEXTURE_TIMESTAMPS_MESSAGE = 'SurfaceTexture update timestamps' |
+_SURFACE_TEXTURE_TIMESTAMP_RE = r'\d+' |
+ |
+ |
+class SurfaceStatsCollector(object): |
+ """Collects surface stats for a SurfaceView from the output of SurfaceFlinger. |
+ |
+ Args: |
+ device: A DeviceUtils instance. |
+ """ |
+ |
+ def __init__(self, device): |
+ # TODO(jbudorick) Remove once telemetry gets switched over. |
+ if isinstance(device, android_commands.AndroidCommands): |
+ device = device_utils.DeviceUtils(device) |
+ self._device = device |
+ self._collector_thread = None |
+ self._surface_before = None |
+ self._get_data_event = None |
+ self._data_queue = None |
+ self._stop_event = None |
+ self._warn_about_empty_data = True |
+ |
+ def DisableWarningAboutEmptyData(self): |
+ self._warn_about_empty_data = False |
+ |
+ def Start(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: |
+ raise Exception('SurfaceFlinger not supported on this device.') |
+ |
+ def Stop(self): |
+ assert self._collector_thread |
+ (refresh_period, timestamps) = self._GetDataFromThread() |
+ if self._collector_thread: |
+ self._stop_event.set() |
+ self._collector_thread.join() |
+ self._collector_thread = None |
+ return (refresh_period, timestamps) |
+ |
+ def _CollectorThread(self): |
+ last_timestamp = 0 |
+ timestamps = [] |
+ retries = 0 |
+ |
+ while not self._stop_event.is_set(): |
+ self._get_data_event.wait(1) |
+ try: |
+ refresh_period, new_timestamps = self._GetSurfaceFlingerFrameData() |
+ if refresh_period is None or timestamps is None: |
+ retries += 1 |
+ if retries < 3: |
+ continue |
+ if last_timestamp: |
+ # Some data has already been collected, but either the app |
+ # was closed or there's no new data. Signal the main thread and |
+ # wait. |
+ self._data_queue.put((None, None)) |
+ self._stop_event.wait() |
+ break |
+ raise Exception('Unable to get surface flinger latency data') |
+ |
+ timestamps += [timestamp for timestamp in new_timestamps |
+ if timestamp > last_timestamp] |
+ if len(timestamps): |
+ last_timestamp = timestamps[-1] |
+ |
+ if self._get_data_event.is_set(): |
+ self._get_data_event.clear() |
+ self._data_queue.put((refresh_period, timestamps)) |
+ timestamps = [] |
+ 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._device.RunShellCommand( |
+ 'dumpsys SurfaceFlinger --latency-clear SurfaceView') |
+ return not len(results) |
+ |
+ def GetSurfaceFlingerPid(self): |
+ results = self._device.RunShellCommand('ps | grep surfaceflinger') |
+ if not results: |
+ raise Exception('Unable to get surface flinger process id') |
+ pid = results[0].split()[1] |
+ return pid |
+ |
+ def _GetSurfaceFlingerFrameData(self): |
+ """Returns collected SurfaceFlinger frame timing data. |
+ |
+ Returns: |
+ A tuple containing: |
+ - The display's nominal refresh period in milliseconds. |
+ - A list of timestamps signifying frame presentation times in |
+ milliseconds. |
+ The return value may be (None, None) if there was no data collected (for |
+ example, if the app was closed before the collector thread has finished). |
+ """ |
+ # 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. |
+ # |
+ # We use the special "SurfaceView" window name because the statistics for |
+ # the activity's main window are not updated when the main web content is |
+ # composited into a SurfaceView. |
+ results = self._device.RunShellCommand( |
+ 'dumpsys SurfaceFlinger --latency SurfaceView') |
+ if not len(results): |
+ return (None, None) |
+ |
+ timestamps = [] |
+ nanoseconds_per_millisecond = 1e6 |
+ refresh_period = long(results[0]) / nanoseconds_per_millisecond |
+ |
+ # If a fence associated with a frame is still pending when we query the |
+ # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX. |
+ # Since we only care about completed frames, we will ignore any timestamps |
+ # with this value. |
+ pending_fence_timestamp = (1 << 63) - 1 |
+ |
+ for line in results[1:]: |
+ fields = line.split() |
+ if len(fields) != 3: |
+ continue |
+ timestamp = long(fields[1]) |
+ if timestamp == pending_fence_timestamp: |
+ continue |
+ timestamp /= nanoseconds_per_millisecond |
+ timestamps.append(timestamp) |
+ |
+ return (refresh_period, timestamps) |