blob: 81412dad475f490d7027e516099af7e76a3f4e7f [file] [log] [blame]
Lajos Molnarb8a45aa2015-10-16 18:47:52 -07001#!/usr/bin/python
2# Copyright (C) 2015 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17import argparse, math, re, sys
18import xml.etree.ElementTree as ET
19from collections import defaultdict, namedtuple
20import itertools
21
22
23def createLookup(values, key):
24 """Creates a lookup table for a collection of values based on keys.
25
26 Arguments:
27 values: a collection of arbitrary values. Must be iterable.
28 key: a function of one argument that returns the key for a value.
29
30 Returns:
31 A dict mapping keys (as generated by the key argument) to lists of
32 values. All values in the lists have the same key, and are in the order
33 they appeared in the collection.
34 """
35 lookup = defaultdict(list)
36 for v in values:
37 lookup[key(v)].append(v)
38 return lookup
39
40
41def _intify(value):
42 """Returns a value converted to int if possible, else the original value."""
43 try:
44 return int(value)
45 except ValueError:
46 return value
47
48
49class Size(namedtuple('Size', ['width', 'height'])):
50 """A namedtuple with width and height fields."""
51 def __str__(self):
52 return '%dx%d' % (self.width, self.height)
53
54
55class _VideoResultBase(object):
56 """Helper methods for results. Not for use by applications.
57
58 Attributes:
59 codec: The name of the codec (string) or None
60 size: Size representing the video size or None
61 mime: The mime-type of the codec (string) or None
62 rates: The measured achievable frame rates
63 is_decoder: True iff codec is a decoder.
64 """
65
66 def __init__(self, is_decoder):
67 self.codec = None
68 self.mime = None
69 self.size = None
70 self._rates_from_failure = []
71 self._rates_from_message = []
72 self.is_decoder = is_decoder
73
74 def _inited(self):
75 """Returns true iff codec, mime and size was set."""
76 return None not in (self.codec, self.mime, self.size)
77
78 def __len__(self):
79 # don't report any result if codec name, mime type and size is unclear
80 if not self._inited():
81 return 0
82 return len(self.rates)
83
84 @property
85 def rates(self):
86 return self._rates_from_failure or self._rates_from_message
87
88 def _parseDict(self, value):
89 """Parses a MediaFormat from its string representation sans brackets."""
90 return dict((k, _intify(v))
91 for k, v in re.findall(r'([^ =]+)=([^ [=]+(?:|\[[^\]]+\]))(?:, |$)', value))
92
93 def _cleanFormat(self, format):
94 """Removes internal fields from a parsed MediaFormat."""
95 format.pop('what', None)
96 format.pop('image-data', None)
97
98 MESSAGE_PATTERN = r'(?P<key>\w+)=(?P<value>\{[^}]*\}|[^ ,{}]+)'
99
100 def _parsePartialResult(self, message_match):
101 """Parses a partial test result conforming to the message pattern.
102
103 Returns:
104 A tuple of string key and int, string or dict value, where dict has
105 string keys mapping to int or string values.
106 """
107 key, value = message_match.group('key', 'value')
108 if value.startswith('{'):
109 value = self._parseDict(value[1:-1])
110 if key.endswith('Format'):
111 self._cleanFormat(value)
112 else:
113 value = _intify(value)
114 return key, value
115
116 def _parseValuesFromBracket(self, line):
117 """Returns the values enclosed in brackets without the brackets.
118
119 Parses a line matching the pattern "<tag>: [<values>]" and returns <values>.
120
121 Raises:
122 ValueError: if the line does not match the pattern.
123 """
124 try:
125 return re.match(r'^[^:]+: *\[(?P<values>.*)\]\.$', line).group('values')
126 except AttributeError:
127 raise ValueError('line does not match "tag: [value]": %s' % line)
128
129 def _parseRawData(self, line):
130 """Parses the raw data line for video performance tests.
131
132 Yields:
133 Dict objects corresponding to parsed results, mapping string keys to
134 int, string or dict values.
135 """
136 try:
137 values = self._parseValuesFromBracket(line)
138 result = {}
139 for m in re.finditer(self.MESSAGE_PATTERN + r'(?P<sep>,? +|$)', values):
140 key, value = self._parsePartialResult(m)
141 result[key] = value
142 if m.group('sep') != ' ':
143 yield result
144 result = {}
145 except ValueError:
146 print >> sys.stderr, 'could not parse line %s' % repr(line)
147
148 def _tryParseMeasuredFrameRate(self, line):
149 """Parses a line starting with 'Measured frame rate:'."""
150 if line.startswith('Measured frame rate: '):
151 try:
152 values = self._parseValuesFromBracket(line)
153 values = re.split(r' *, *', values)
154 self._rates_from_failure = list(map(float, values))
155 except ValueError:
156 print >> sys.stderr, 'could not parse line %s' % repr(line)
157
158 def parse(self, test):
159 """Parses the ValueArray and FailedScene lines of a test result.
160
161 Arguments:
162 test: An ElementTree <Test> element.
163 """
164 failure = test.find('FailedScene')
165 if failure is not None:
166 trace = failure.find('StackTrace')
167 if trace is not None:
168 for line in re.split(r'[\r\n]+', trace.text):
169 self._parseFailureLine(line)
170 details = test.find('Details')
171 if details is not None:
172 for array in details.iter('ValueArray'):
173 message = array.get('message')
174 self._parseMessage(message, array)
175
176 def _parseFailureLine(self, line):
177 raise NotImplementedError
178
179 def _parseMessage(self, message, array):
180 raise NotImplementedError
181
182 def getData(self):
183 """Gets the parsed test result data.
184
185 Yields:
186 Result objects containing at least codec, size, mime and rates attributes."""
187 yield self
188
189
190class VideoEncoderDecoderTestResult(_VideoResultBase):
191 """Represents a result from a VideoEncoderDecoderTest performance case."""
192
193 def __init__(self, unused_m):
194 super(VideoEncoderDecoderTestResult, self).__init__(is_decoder=False)
195
196 # If a VideoEncoderDecoderTest succeeds, it provides the results in the
197 # message of a ValueArray. If fails, it provides the results in the failure
198 # using raw data. (For now it also includes some data in the ValueArrays even
199 # if it fails, which we ignore.)
200
201 def _parseFailureLine(self, line):
202 """Handles parsing a line from the failure log."""
203 self._tryParseMeasuredFrameRate(line)
204
205 def _parseMessage(self, message, array):
206 """Handles parsing a message from ValueArrays."""
207 if message.startswith('codec='):
208 result = dict(self._parsePartialResult(m)
209 for m in re.finditer(self.MESSAGE_PATTERN + '(?: |$)', message))
210 if 'EncInputFormat' in result:
211 self.codec = result['codec']
212 fmt = result['EncInputFormat']
213 self.size = Size(fmt['width'], fmt['height'])
214 self.mime = result['EncOutputFormat']['mime']
215 self._rates_from_message.append(1000000./result['min'])
216
217
218class VideoDecoderPerfTestResult(_VideoResultBase):
219 """Represents a result from a VideoDecoderPerfTest performance case."""
220
221 # If a VideoDecoderPerfTest succeeds, it provides the results in the message
222 # of a ValueArray. If fails, it provides the results in the failure only
223 # using raw data.
224
225 def __init__(self, unused_m):
226 super(VideoDecoderPerfTestResult, self).__init__(is_decoder=True)
227
228 def _parseFailureLine(self, line):
229 """Handles parsing a line from the failure log."""
230 self._tryParseMeasuredFrameRate(line)
231 # if the test failed, we can only get the codec/size/mime from the raw data.
232 if line.startswith('Raw data: '):
233 for result in self._parseRawData(line):
234 fmt = result['DecOutputFormat']
235 self.size = Size(fmt['width'], fmt['height'])
236 self.codec = result['codec']
237 self.mime = result['mime']
238
239 def _parseMessage(self, message, array):
240 """Handles parsing a message from ValueArrays."""
241 if message.startswith('codec='):
242 result = dict(self._parsePartialResult(m)
243 for m in re.finditer(self.MESSAGE_PATTERN + '(?: |$)', message))
244 if result.get('decodeto') == 'surface':
245 self.codec = result['codec']
246 fmt = result['DecOutputFormat']
247 self.size = Size(fmt['width'], fmt['height'])
248 self.mime = result['mime']
249 self._rates_from_message.append(1000000. / result['min'])
250
251
252class Results(object):
253 """Container that keeps all test results."""
254 def __init__(self):
255 self._results = [] # namedtuples
256 self._device = None
257
258 VIDEO_ENCODER_DECODER_TEST_REGEX = re.compile(
259 'test(.*)(\d{4})x(\d{4})(Goog|Other)$')
260
261 VIDEO_DECODER_PERF_TEST_REGEX = re.compile(
262 'test(VP[89]|H26[34]|MPEG4|HEVC)(\d+)x(\d+)(.*)$')
263
264 TestCaseSpec = namedtuple('TestCaseSpec', 'package path class_ regex result_class')
265
266 def _getTestCases(self):
267 return [
268 self.TestCaseSpec(package='CtsDeviceVideoPerf',
269 path='TestSuite/TestSuite/TestSuite/TestSuite/TestCase',
270 class_='VideoEncoderDecoderTest',
271 regex=self.VIDEO_ENCODER_DECODER_TEST_REGEX,
272 result_class=VideoEncoderDecoderTestResult),
273 self.TestCaseSpec(package='CtsMediaTestCases',
274 path='TestSuite/TestSuite/TestSuite/TestCase',
275 class_='VideoDecoderPerfTest',
276 regex=self.VIDEO_DECODER_PERF_TEST_REGEX,
277 result_class=VideoDecoderPerfTestResult)
278 ]
279
280 def _verifyDeviceInfo(self, device):
281 assert self._device in (None, device), "expected %s device" % self._device
282 self._device = device
283
284 def importXml(self, xml):
285 self._verifyDeviceInfo(xml.find('DeviceInfo/BuildInfo').get('buildName'))
286
287 packages = createLookup(self._getTestCases(), lambda tc: tc.package)
288
289 for pkg in xml.iter('TestPackage'):
290 tests_in_package = packages.get(pkg.get('name'))
291 if not tests_in_package:
292 continue
293 paths = createLookup(tests_in_package, lambda tc: tc.path)
294 for path, tests_in_path in paths.items():
295 classes = createLookup(tests_in_path, lambda tc: tc.class_)
296 for tc in pkg.iterfind(path):
297 tests_in_class = classes.get(tc.get('name'))
298 if not tests_in_class:
299 continue
300 for test in tc.iter('Test'):
301 for tc in tests_in_class:
302 m = tc.regex.match(test.get('name'))
303 if m:
304 result = tc.result_class(m)
305 result.parse(test)
306 self._results.append(result)
307
308 def importFile(self, path):
309 print >> sys.stderr, 'Importing "%s"...' % path
310 try:
311 return self.importXml(ET.parse(path))
312 except ET.ParseError:
313 raise ValueError('not a valid XML file')
314
315 def getData(self):
316 for result in self._results:
317 for data in result.getData():
318 yield data
319
320 def dumpXml(self, results):
321 yield '<?xml version="1.0" encoding="utf-8" ?>'
322 yield '<!-- Copyright 2015 The Android Open Source Project'
323 yield ''
324 yield ' Licensed under the Apache License, Version 2.0 (the "License");'
325 yield ' you may not use this file except in compliance with the License.'
326 yield ' You may obtain a copy of the License at'
327 yield ''
328 yield ' http://www.apache.org/licenses/LICENSE-2.0'
329 yield ''
330 yield ' Unless required by applicable law or agreed to in writing, software'
331 yield ' distributed under the License is distributed on an "AS IS" BASIS,'
332 yield ' WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.'
333 yield ' See the License for the specific language governing permissions and'
334 yield ' limitations under the License.'
335 yield '-->'
336 yield ''
337 yield '<MediaCodecs>'
338 last_section = None
339 Comp = namedtuple('Comp', 'is_decoder google mime name')
340 by_comp = createLookup(results,
341 lambda e: Comp(is_decoder=e.is_decoder, google='.google.' in e.codec, mime=e.mime, name=e.codec))
342 for comp in sorted(by_comp):
343 section = 'Decoders' if comp.is_decoder else 'Encoders'
344 if section != last_section:
345 if last_section:
346 yield ' </%s>' % last_section
347 yield ' <%s>' % section
348 last_section = section
349 yield ' <MediaCodec name="%s" type="%s" update="true">' % (comp.name, comp.mime)
350 by_size = createLookup(by_comp[comp], lambda e: e.size)
351 for size in sorted(by_size):
352 values = list(itertools.chain(*(e.rates for e in by_size[size])))
353 min_, max_ = min(values), max(values)
354 med_ = int(math.sqrt(min_ * max_))
355 yield ' <Limit name="measured-frame-rate-%s" range="%d-%d" />' % (size, med_, med_)
356 yield ' </MediaCodec>'
357 if last_section:
358 yield ' </%s>' % last_section
359 yield '</MediaCodecs>'
360
361
362class Main(object):
363 """Executor of this utility."""
364
365 def __init__(self):
366 self._result = Results()
367
368 self._parser = argparse.ArgumentParser('get_achievable_framerates')
369 self._parser.add_argument('result_xml', nargs='+')
370
371 def _parseArgs(self):
372 self._args = self._parser.parse_args()
373
374 def _importXml(self, xml):
375 self._result.importFile(xml)
376
377 def _report(self):
378 for line in self._result.dumpXml(r for r in self._result.getData() if r):
379 print line
380
381 def run(self):
382 self._parseArgs()
383 try:
384 for xml in self._args.result_xml:
385 try:
386 self._importXml(xml)
387 except (ValueError, IOError, AssertionError) as e:
388 print >> sys.stderr, e
389 raise KeyboardInterrupt
390 self._report()
391 except KeyboardInterrupt:
392 print >> sys.stderr, 'Interrupted.'
393
394if __name__ == '__main__':
395 Main().run()
396