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 |
index c7e7527a1e25c2ef663f5f80c09f6f80696bbba7..499b0c6fd25bad3fdb3e8a89a227097654e56d5f 100644 |
--- a/build/android/pylib/perf/surface_stats_collector.py |
+++ b/build/android/pylib/perf/surface_stats_collector.py |
@@ -15,6 +15,8 @@ |
_SURFACE_TEXTURE_TIMESTAMPS_MESSAGE = 'SurfaceTexture update timestamps' |
_SURFACE_TEXTURE_TIMESTAMP_RE = r'\d+' |
+_MIN_NORMALIZED_FRAME_LENGTH = 0.5 |
+ |
class SurfaceStatsCollector(object): |
"""Collects surface stats for a SurfaceView from the output of SurfaceFlinger. |
@@ -22,6 +24,11 @@ |
Args: |
device: A DeviceUtils instance. |
""" |
+ class Result(object): |
+ def __init__(self, name, value, unit): |
+ self.name = name |
+ self.value = value |
+ self.unit = unit |
def __init__(self, device): |
# TODO(jbudorick) Remove once telemetry gets switched over. |
@@ -29,10 +36,12 @@ |
device = device_utils.DeviceUtils(device) |
self._device = device |
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 |
+ self._results = [] |
self._warn_about_empty_data = True |
def DisableWarningAboutEmptyData(self): |
@@ -48,16 +57,110 @@ |
self._collector_thread = threading.Thread(target=self._CollectorThread) |
self._collector_thread.start() |
else: |
- raise Exception('SurfaceFlinger not supported on this device.') |
+ self._use_legacy_method = True |
+ self._surface_before = self._GetSurfaceStatsLegacy() |
def Stop(self): |
- assert self._collector_thread |
- (refresh_period, timestamps) = self._GetDataFromThread() |
+ self._StorePerfResults() |
if self._collector_thread: |
self._stop_event.set() |
self._collector_thread.join() |
self._collector_thread = None |
- return (refresh_period, timestamps) |
+ |
+ def SampleResults(self): |
+ self._StorePerfResults() |
+ results = self.GetResults() |
+ self._results = [] |
+ return results |
+ |
+ def GetResults(self): |
+ return self._results or self._GetEmptyResults() |
+ |
+ @staticmethod |
+ def _GetEmptyResults(): |
+ return [ |
+ SurfaceStatsCollector.Result('refresh_period', None, 'seconds'), |
+ SurfaceStatsCollector.Result('jank_count', None, 'janks'), |
+ SurfaceStatsCollector.Result('max_frame_delay', None, 'vsyncs'), |
+ SurfaceStatsCollector.Result('frame_lengths', None, 'vsyncs'), |
+ SurfaceStatsCollector.Result('avg_surface_fps', None, 'fps') |
+ ] |
+ |
+ @staticmethod |
+ def _GetNormalizedDeltas(data, refresh_period, min_normalized_delta=None): |
+ deltas = [t2 - t1 for t1, t2 in zip(data, data[1:])] |
+ if min_normalized_delta != None: |
+ deltas = [d for d in deltas |
+ if d / refresh_period >= min_normalized_delta] |
+ return (deltas, [delta / refresh_period for delta in deltas]) |
+ |
+ @staticmethod |
+ def _CalculateResults(refresh_period, timestamps, result_suffix): |
+ """Returns a list of SurfaceStatsCollector.Result.""" |
+ frame_count = len(timestamps) |
+ seconds = timestamps[-1] - timestamps[0] |
+ |
+ frame_lengths, normalized_frame_lengths = \ |
+ SurfaceStatsCollector._GetNormalizedDeltas( |
+ timestamps, refresh_period, _MIN_NORMALIZED_FRAME_LENGTH) |
+ if len(frame_lengths) < frame_count - 1: |
+ logging.warning('Skipping frame lengths that are too short.') |
+ frame_count = len(frame_lengths) + 1 |
+ if len(frame_lengths) == 0: |
+ raise Exception('No valid frames lengths found.') |
+ _, normalized_changes = \ |
+ SurfaceStatsCollector._GetNormalizedDeltas( |
+ frame_lengths, refresh_period) |
+ jankiness = [max(0, round(change)) for change in normalized_changes] |
+ pause_threshold = 20 |
+ jank_count = sum(1 for change in jankiness |
+ if change > 0 and change < pause_threshold) |
+ return [ |
+ SurfaceStatsCollector.Result( |
+ 'avg_surface_fps' + result_suffix, |
+ int(round((frame_count - 1) / seconds)), 'fps'), |
+ SurfaceStatsCollector.Result( |
+ 'jank_count' + result_suffix, jank_count, 'janks'), |
+ SurfaceStatsCollector.Result( |
+ 'max_frame_delay' + result_suffix, |
+ round(max(normalized_frame_lengths)), |
+ 'vsyncs'), |
+ SurfaceStatsCollector.Result( |
+ 'frame_lengths' + result_suffix, normalized_frame_lengths, |
+ 'vsyncs'), |
+ ] |
+ |
+ @staticmethod |
+ def _CalculateBuckets(refresh_period, timestamps): |
+ results = [] |
+ for pct in [0.99, 0.5]: |
+ sliced = timestamps[min(int(-pct * len(timestamps)), -3) : ] |
+ results += SurfaceStatsCollector._CalculateResults( |
+ refresh_period, sliced, '_' + str(int(pct * 100))) |
+ return results |
+ |
+ def _StorePerfResults(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']) |
+ self._results.append(SurfaceStatsCollector.Result( |
+ 'avg_surface_fps', int(round(frame_count / seconds)), 'fps')) |
+ return |
+ |
+ # Non-legacy method. |
+ assert self._collector_thread |
+ (refresh_period, timestamps) = self._GetDataFromThread() |
+ if not refresh_period or not len(timestamps) >= 3: |
+ if self._warn_about_empty_data: |
+ logging.warning('Surface stat data is empty') |
+ return |
+ self._results.append(SurfaceStatsCollector.Result( |
+ 'refresh_period', refresh_period, 'seconds')) |
+ self._results += self._CalculateResults(refresh_period, timestamps, '') |
+ self._results += self._CalculateBuckets(refresh_period, timestamps) |
def _CollectorThread(self): |
last_timestamp = 0 |
@@ -116,21 +219,13 @@ |
'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 display's nominal refresh period in seconds. |
+ - A list of timestamps signifying frame presentation times in seconds. |
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). |
""" |
@@ -169,8 +264,8 @@ |
return (None, None) |
timestamps = [] |
- nanoseconds_per_millisecond = 1e6 |
- refresh_period = long(results[0]) / nanoseconds_per_millisecond |
+ nanoseconds_per_second = 1e9 |
+ refresh_period = long(results[0]) / nanoseconds_per_second |
# 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. |
@@ -185,7 +280,33 @@ |
timestamp = long(fields[1]) |
if timestamp == pending_fence_timestamp: |
continue |
- timestamp /= nanoseconds_per_millisecond |
+ timestamp /= nanoseconds_per_second |
timestamps.append(timestamp) |
return (refresh_period, timestamps) |
+ |
+ 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._device.RunShellCommand('service call SurfaceFlinger 1013') |
+ assert len(results) == 1 |
+ match = re.search(r'^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(), |
+ } |