blob: 665135a5b825fbc1813143710a568af51efb29f5 [file] [log] [blame]
Sergei Trofimov195085e2017-06-06 14:41:54 +01001import csv
2import logging
3import os
4import re
5import shutil
6import sys
7import tempfile
8import threading
9import time
10from collections import namedtuple, OrderedDict
11from distutils.version import LooseVersion
12
13from devlib.exception import WorkerThreadError, TargetNotRespondingError, TimeoutError
14
15
16logger = logging.getLogger('rendering')
17
Sergei Trofimov2b3cee62017-06-06 14:47:42 +010018SurfaceFlingerFrame = namedtuple('SurfaceFlingerFrame',
19 'desired_present_time actual_present_time frame_ready_time')
20
Sergei Trofimov195085e2017-06-06 14:41:54 +010021
22class FrameCollector(threading.Thread):
23
24 def __init__(self, target, period):
25 super(FrameCollector, self).__init__()
26 self.target = target
27 self.period = period
28 self.stop_signal = threading.Event()
29 self.frames = []
30
31 self.temp_file = None
32 self.refresh_period = None
33 self.drop_threshold = None
34 self.unresponsive_count = 0
35 self.last_ready_time = None
36 self.exc = None
37 self.header = None
38
39 def run(self):
40 logger.debug('Surface flinger frame data collection started.')
41 try:
42 self.stop_signal.clear()
43 fd, self.temp_file = tempfile.mkstemp()
44 logger.debug('temp file: {}'.format(self.temp_file))
45 wfh = os.fdopen(fd, 'wb')
46 try:
47 while not self.stop_signal.is_set():
48 self.collect_frames(wfh)
49 time.sleep(self.period)
50 finally:
51 wfh.close()
52 except (TargetNotRespondingError, TimeoutError): # pylint: disable=W0703
53 raise
54 except Exception, e: # pylint: disable=W0703
55 logger.warning('Exception on collector thread: {}({})'.format(e.__class__.__name__, e))
56 self.exc = WorkerThreadError(self.name, sys.exc_info())
57 logger.debug('Surface flinger frame data collection stopped.')
58
59 def stop(self):
60 self.stop_signal.set()
61 self.join()
62 if self.unresponsive_count:
63 message = 'FrameCollector was unrepsonsive {} times.'.format(self.unresponsive_count)
64 if self.unresponsive_count > 10:
65 logger.warning(message)
66 else:
67 logger.debug(message)
68 if self.exc:
69 raise self.exc # pylint: disable=E0702
70
71 def process_frames(self, outfile=None):
72 if not self.temp_file:
73 raise RuntimeError('Attempting to process frames before running the collector')
74 with open(self.temp_file) as fh:
75 self._process_raw_file(fh)
76 if outfile:
77 shutil.copy(self.temp_file, outfile)
78 os.unlink(self.temp_file)
79 self.temp_file = None
80
81 def write_frames(self, outfile, columns=None):
82 if columns is None:
83 header = self.header
84 frames = self.frames
85 else:
Sergei Trofimovd952abf2017-09-19 13:48:08 +010086 indexes = []
87 for c in columns:
88 if c not in self.header:
89 msg = 'Invalid column "{}"; must be in {}'
90 raise ValueError(msg.format(c, self.header))
91 indexes.append(self.header.index(c))
Sergei Trofimov195085e2017-06-06 14:41:54 +010092 frames = [[f[i] for i in indexes] for f in self.frames]
Sergei Trofimovd952abf2017-09-19 13:48:08 +010093 header = columns
Sergei Trofimov195085e2017-06-06 14:41:54 +010094 with open(outfile, 'w') as wfh:
95 writer = csv.writer(wfh)
96 if header:
97 writer.writerow(header)
98 writer.writerows(frames)
99
100 def collect_frames(self, wfh):
101 raise NotImplementedError()
102
103 def clear(self):
104 raise NotImplementedError()
105
106 def _process_raw_file(self, fh):
107 raise NotImplementedError()
108
109
Sergei Trofimov2b3cee62017-06-06 14:47:42 +0100110class SurfaceFlingerFrameCollector(FrameCollector):
111
112 def __init__(self, target, period, view, header=None):
113 super(SurfaceFlingerFrameCollector, self).__init__(target, period)
114 self.view = view
115 self.header = header or SurfaceFlingerFrame._fields
116
117 def collect_frames(self, wfh):
118 for activity in self.list():
119 if activity == self.view:
120 wfh.write(self.get_latencies(activity))
121
122 def clear(self):
123 self.target.execute('dumpsys SurfaceFlinger --latency-clear ')
124
125 def get_latencies(self, activity):
126 cmd = 'dumpsys SurfaceFlinger --latency "{}"'
127 return self.target.execute(cmd.format(activity))
128
129 def list(self):
Sergei Trofimov50dfb292017-09-13 11:36:41 +0100130 text = self.target.execute('dumpsys SurfaceFlinger --list')
131 return text.replace('\r\n', '\n').replace('\r', '\n').split('\n')
Sergei Trofimov2b3cee62017-06-06 14:47:42 +0100132
133 def _process_raw_file(self, fh):
134 text = fh.read().replace('\r\n', '\n').replace('\r', '\n')
135 for line in text.split('\n'):
136 line = line.strip()
137 if line:
138 self._process_trace_line(line)
139
140 def _process_trace_line(self, line):
141 parts = line.split()
142 if len(parts) == 3:
143 frame = SurfaceFlingerFrame(*map(int, parts))
144 if not frame.frame_ready_time:
145 return # "null" frame
146 if frame.frame_ready_time <= self.last_ready_time:
147 return # duplicate frame
148 if (frame.frame_ready_time - frame.desired_present_time) > self.drop_threshold:
149 logger.debug('Dropping bogus frame {}.'.format(line))
150 return # bogus data
151 self.last_ready_time = frame.frame_ready_time
152 self.frames.append(frame)
153 elif len(parts) == 1:
154 self.refresh_period = int(parts[0])
155 self.drop_threshold = self.refresh_period * 1000
156 elif 'SurfaceFlinger appears to be unresponsive, dumping anyways' in line:
157 self.unresponsive_count += 1
158 else:
159 logger.warning('Unexpected SurfaceFlinger dump output: {}'.format(line))
160
161
Sergei Trofimov195085e2017-06-06 14:41:54 +0100162def read_gfxinfo_columns(target):
163 output = target.execute('dumpsys gfxinfo --list framestats')
164 lines = iter(output.split('\n'))
165 for line in lines:
166 if line.startswith('---PROFILEDATA---'):
167 break
168 columns_line = lines.next()
169 return columns_line.split(',')[:-1] # has a trailing ','
170
171
172class GfxinfoFrameCollector(FrameCollector):
173
174 def __init__(self, target, period, package, header=None):
175 super(GfxinfoFrameCollector, self).__init__(target, period)
176 self.package = package
177 self.header = None
178 self._init_header(header)
179
180 def collect_frames(self, wfh):
181 cmd = 'dumpsys gfxinfo {} framestats'
182 wfh.write(self.target.execute(cmd.format(self.package)))
183
184 def clear(self):
185 pass
186
187 def _init_header(self, header):
188 if header is not None:
189 self.header = header
190 else:
191 self.header = read_gfxinfo_columns(self.target)
192
193 def _process_raw_file(self, fh):
194 found = False
195 try:
196 while True:
197 for line in fh:
198 if line.startswith('---PROFILEDATA---'):
199 found = True
200 break
201
202 fh.next() # headers
203 for line in fh:
204 if line.startswith('---PROFILEDATA---'):
205 break
206 self.frames.append(map(int, line.strip().split(',')[:-1])) # has a trailing ','
207 except StopIteration:
208 pass
209 if not found:
210 logger.warning('Could not find frames data in gfxinfo output')
211 return
Sergei Trofimova8ca0fc2017-09-07 14:34:55 +0100212
213
214def _file_reverse_iter(fh, buf_size=1024):
215 fh.seek(0, os.SEEK_END)
216 offset = 0
217 file_size = remaining_size = fh.tell()
218 while remaining_size > 0:
219 offset = min(file_size, offset + buf_size)
220 fh.seek(file_size - offset)
221 buf = fh.read(min(remaining_size, buf_size))
222 remaining_size -= buf_size
223 yield buf
224
225
226def gfxinfo_get_last_dump(filepath):
227 """
228 Return the last gfxinfo dump from the frame collector's raw output.
229
230 """
231 record = ''
232 with open(filepath, 'r') as fh:
233 fh_iter = _file_reverse_iter(fh)
234 try:
235 while True:
236 buf = fh_iter.next()
237 ix = buf.find('** Graphics')
238 if ix >= 0:
239 return buf[ix:] + record
240
241 ix = buf.find(' **\n')
242 if ix >= 0:
243 buf = fh_iter.next() + buf
244 ix = buf.find('** Graphics')
245 if ix < 0:
246 msg = '"{}" appears to be corrupted'
247 raise RuntimeError(msg.format(filepath))
248 return buf[ix:] + record
249 record = buf + record
250 except StopIteration:
251 pass