rendering: Add surfaceflinger frame collector

Add a FrameCollector implementation that utilizes the output from
surfaceflinger (the Android compositor). This is is the only method for
accessing rendering data on older Android devices.
diff --git a/devlib/utils/rendering.py b/devlib/utils/rendering.py
index 53dd238..3b7b6c4 100644
--- a/devlib/utils/rendering.py
+++ b/devlib/utils/rendering.py
@@ -15,6 +15,9 @@
 
 logger = logging.getLogger('rendering')
 
+SurfaceFlingerFrame = namedtuple('SurfaceFlingerFrame',
+                                 'desired_present_time actual_present_time frame_ready_time')
+
 
 class FrameCollector(threading.Thread):
 
@@ -99,6 +102,57 @@
         raise NotImplementedError()
 
 
+class SurfaceFlingerFrameCollector(FrameCollector):
+
+    def __init__(self, target, period, view, header=None):
+        super(SurfaceFlingerFrameCollector, self).__init__(target, period)
+        self.view = view
+        self.header = header or SurfaceFlingerFrame._fields
+
+    def collect_frames(self, wfh):
+        for activity in self.list():
+            if activity == self.view:
+                wfh.write(self.get_latencies(activity))
+
+    def clear(self):
+        self.target.execute('dumpsys SurfaceFlinger --latency-clear ')
+
+    def get_latencies(self, activity):
+        cmd = 'dumpsys SurfaceFlinger --latency "{}"'
+        return self.target.execute(cmd.format(activity))
+
+    def list(self):
+        return self.target.execute('dumpsys SurfaceFlinger --list').split('\r\n')
+
+    def _process_raw_file(self, fh):
+        text = fh.read().replace('\r\n', '\n').replace('\r', '\n')
+        for line in text.split('\n'):
+            line = line.strip()
+            if line:
+                self._process_trace_line(line)
+
+    def _process_trace_line(self, line):
+        parts = line.split()
+        if len(parts) == 3:
+            frame = SurfaceFlingerFrame(*map(int, parts))
+            if not frame.frame_ready_time:
+                return # "null" frame
+            if frame.frame_ready_time <= self.last_ready_time:
+                return  # duplicate frame
+            if (frame.frame_ready_time - frame.desired_present_time) > self.drop_threshold:
+                logger.debug('Dropping bogus frame {}.'.format(line))
+                return  # bogus data
+            self.last_ready_time = frame.frame_ready_time
+            self.frames.append(frame)
+        elif len(parts) == 1:
+            self.refresh_period = int(parts[0])
+            self.drop_threshold = self.refresh_period * 1000
+        elif 'SurfaceFlinger appears to be unresponsive, dumping anyways' in line:
+            self.unresponsive_count += 1
+        else:
+            logger.warning('Unexpected SurfaceFlinger dump output: {}'.format(line))
+
+
 def read_gfxinfo_columns(target):
     output = target.execute('dumpsys gfxinfo --list framestats')
     lines = iter(output.split('\n'))