You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
360 lines
13 KiB
360 lines
13 KiB
#!/usr/bin/python
|
|
# Copyright (C) 2015 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
import argparse, json, math, re, sys, zipfile
|
|
import xml.etree.ElementTree as ET
|
|
from collections import defaultdict, namedtuple
|
|
|
|
|
|
class Size(namedtuple('Size', ['width', 'height'])):
|
|
"""A namedtuple with width and height fields."""
|
|
def __str__(self):
|
|
return '%dx%d' % (self.width, self.height)
|
|
|
|
def nicekey(v):
|
|
"""Returns a nicer sort key for sorting strings.
|
|
|
|
This sorts using lower case, with numbers in numerical order first."""
|
|
key = []
|
|
num = False
|
|
for p in re.split('(\d+)', v.lower()):
|
|
if num:
|
|
key.append(('0', int(p)))
|
|
elif p:
|
|
key.append((p, 0))
|
|
num = not num
|
|
return key + [(v, 0)]
|
|
|
|
def nice(v):
|
|
"""Returns a nicer representation for objects in debug messages.
|
|
|
|
Dictionaries are sorted, size is WxH, unicode removed, and floats have 1 digit precision."""
|
|
if isinstance(v, dict):
|
|
return 'dict(' + ', '.join(k + '=' + nice(v) for k, v in sorted(v.items(), key=lambda i: nicekey(i[0]))) + ')'
|
|
if isinstance(v, str):
|
|
return repr(v)
|
|
if isinstance(v, int):
|
|
return str(v)
|
|
if isinstance(v, Size):
|
|
return repr(str(v))
|
|
if isinstance(v, float):
|
|
return '%.1f' % v
|
|
if isinstance(v, type(u'')):
|
|
return repr(str(v))
|
|
raise ValueError(v)
|
|
|
|
class ResultParser:
|
|
@staticmethod
|
|
def _intify(value):
|
|
"""Returns a value converted to int if possible, else the original value."""
|
|
try:
|
|
return int(value)
|
|
except ValueError:
|
|
return value
|
|
|
|
def _parseDict(self, value):
|
|
"""Parses a MediaFormat from its string representation sans brackets."""
|
|
return dict((k, self._intify(v))
|
|
for k, v in re.findall(r'([^ =]+)=([^ [=]+(?:|\[[^\]]+\]))(?:, |$)', value))
|
|
|
|
def _cleanFormat(self, format):
|
|
"""Removes internal fields from a parsed MediaFormat."""
|
|
format.pop('what', None)
|
|
format.pop('image-data', None)
|
|
|
|
MESSAGE_PATTERN = r'(?P<key>\w+)=(?P<value>\{[^}]*\}|[^ ,{}]+)'
|
|
|
|
def _parsePartialResult(self, message_match):
|
|
"""Parses a partial test result conforming to the message pattern.
|
|
|
|
Returns:
|
|
A tuple of string key and int, string or dict value, where dict has
|
|
string keys mapping to int or string values.
|
|
"""
|
|
key, value = message_match.group('key', 'value')
|
|
if value.startswith('{'):
|
|
value = self._parseDict(value[1:-1])
|
|
if key.endswith('Format'):
|
|
self._cleanFormat(value)
|
|
else:
|
|
value = self._intify(value)
|
|
return key, value
|
|
|
|
|
|
def perc(data, p, fn=round):
|
|
"""Returns a percentile value from a sorted array.
|
|
|
|
Arguments:
|
|
data: sorted data
|
|
p: percentile value (0-100)
|
|
fn: method used for rounding the percentile to an integer index in data
|
|
"""
|
|
return data[int(fn((len(data) - 1) * p / 100))]
|
|
|
|
|
|
def genXml(data, A=None):
|
|
yield '<?xml version="1.0" encoding="utf-8" ?>'
|
|
yield '<!-- Copyright 2016 The Android Open Source Project'
|
|
yield ''
|
|
yield ' Licensed under the Apache License, Version 2.0 (the "License");'
|
|
yield ' you may not use this file except in compliance with the License.'
|
|
yield ' You may obtain a copy of the License at'
|
|
yield ''
|
|
yield ' http://www.apache.org/licenses/LICENSE-2.0'
|
|
yield ''
|
|
yield ' Unless required by applicable law or agreed to in writing, software'
|
|
yield ' distributed under the License is distributed on an "AS IS" BASIS,'
|
|
yield ' WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.'
|
|
yield ' See the License for the specific language governing permissions and'
|
|
yield ' limitations under the License.'
|
|
yield '-->'
|
|
yield ''
|
|
yield '<MediaCodecs>'
|
|
last_section = None
|
|
from collections import namedtuple
|
|
Comp = namedtuple('Comp', 'is_decoder google mime name')
|
|
Result = namedtuple('Result', 'mn mx p95 med geo p5')
|
|
for comp_, cdata in sorted(data.items()):
|
|
comp = Comp(*comp_)
|
|
section = 'Decoders' if comp.is_decoder else 'Encoders'
|
|
if section != last_section:
|
|
if last_section:
|
|
yield ' </%s>' % last_section
|
|
yield ' <%s>' % section
|
|
last_section = section
|
|
yield ' <MediaCodec name="%s" type="%s" update="true">' % (comp.name, comp.mime)
|
|
for size, sdata in sorted(cdata.items()):
|
|
data = sorted(sdata)
|
|
N = len(data)
|
|
mn, mx = data[0], data[-1]
|
|
|
|
if N < 20 and not A.ignore:
|
|
raise ValueError("need at least 20 data points for %s size %s; have %s" %
|
|
(comp.name, size, N))
|
|
|
|
TO = 2.2 # tolerance with margin
|
|
T = TO / 1.1 # tolerance without margin
|
|
|
|
Final = namedtuple('Final', 'comment c2 var qual')
|
|
lastFinal = None
|
|
for RG in (10, 15, 20, 25, 30, 40, 50):
|
|
P = 50./RG
|
|
quality = 0
|
|
p95, med, p5 = perc(data, P, math.floor), perc(data, 50, round), perc(data, 100 - P, math.ceil)
|
|
geo = math.sqrt(p5 * p95)
|
|
comment = ''
|
|
pub_lo, pub_hi = min(int(p95 * T), round(geo)), max(math.ceil(p5 / T), round(geo))
|
|
if pub_lo > med:
|
|
if pub_lo > med * 1.1:
|
|
quality += 0.5
|
|
comment += ' SLOW'
|
|
pub_lo = int(med)
|
|
if N < 2 * RG:
|
|
comment += ' N=%d' % N
|
|
quality += 2
|
|
RGVAR = False
|
|
if p5 / p95 > T ** 3:
|
|
quality += 3
|
|
RGVAR = True
|
|
if pub_hi > pub_lo * TO:
|
|
quality += 1
|
|
if RG == 10:
|
|
# find best pub_lo and pub_hi
|
|
for i in range(N / 2):
|
|
pub_lo_, pub_hi_ = min(int(data[N / 2 - i - 1] * T), round(geo), int(med)), max(math.ceil(data[N / 2 + i] / T), round(geo))
|
|
if pub_hi_ > pub_lo_ * TO:
|
|
# ???
|
|
pub_lo = min(pub_lo, math.ceil(pub_hi_ / TO))
|
|
break
|
|
pub_lo, pub_hi = pub_lo_, pub_hi_
|
|
if mn < pub_lo / T or mx > pub_hi * T or pub_lo <= pub_hi / T:
|
|
quality += 1
|
|
comment += ' FLAKY('
|
|
if round(mn, 1) < pub_lo / T:
|
|
comment += 'mn=%.1f < ' % mn
|
|
comment += 'RANGE'
|
|
if round(mx, 1) > pub_hi * T:
|
|
comment += ' < mx=%.1f' % mx
|
|
comment += ')'
|
|
if False:
|
|
comment += ' DATA(mn=%1.f p%d=%1.f accept=%1.f-%1.f p50=%1.f p%d=%1.f mx=%1.f)' % (
|
|
mn, 100-P, p95, pub_lo / T, pub_hi * T, med, P, p5, mx)
|
|
var = math.sqrt(p5/p95)
|
|
if p95 < geo / T or p5 > geo * T:
|
|
if RGVAR:
|
|
comment += ' RG.VARIANCE:%.1f' % ((p5/p95) ** (1./3))
|
|
else:
|
|
comment += ' variance:%.1f' % var
|
|
comment = comment.replace('RANGE', '%d - %d' % (math.ceil(pub_lo / T), int(pub_hi * T)))
|
|
c2 = ''
|
|
if N >= 2 * RG:
|
|
c2 += ' N=%d' % N
|
|
if var <= T or p5 / p95 > T ** 3:
|
|
c2 += ' v%d%%=%.1f' % (round(100 - 2 * P), var)
|
|
if A and A.dbg:
|
|
c2 += ' E=%s' % (str(quality))
|
|
if c2:
|
|
c2 = ' <!--%s -->' % c2
|
|
|
|
if comment:
|
|
comment = ' <!-- measured %d%%:%d-%d med:%d%s -->' % (round(100 - 2 * P), int(p95), math.ceil(p5), int(round(med)), comment)
|
|
if A and A.dbg: yield '<!-- --> %s%s' % (comment, c2)
|
|
c2 = ' <Limit name="measured-frame-rate-%s" range="%d-%d" />%s' % (size, pub_lo, pub_hi, c2)
|
|
final = Final(comment, c2, var, quality)
|
|
if lastFinal and final.var > lastFinal.var * math.sqrt(1.3):
|
|
if A and A.dbg: yield '<!-- RANGE JUMP -->'
|
|
break
|
|
elif not lastFinal or quality <= lastFinal.qual:
|
|
lastFinal = final
|
|
if N < 2 * RG or quality >= 4:
|
|
break
|
|
comment, c2, var, quality = lastFinal
|
|
|
|
if comment:
|
|
yield comment
|
|
yield c2
|
|
yield ' </MediaCodec>'
|
|
if last_section:
|
|
yield ' </%s>' % last_section
|
|
yield '</MediaCodecs>'
|
|
|
|
|
|
class Data:
|
|
def __init__(self):
|
|
self.data = set()
|
|
self.kind = {}
|
|
self.devices = set()
|
|
self.parser = ResultParser()
|
|
|
|
def summarize(self, A=None):
|
|
devs = sorted(self.devices)
|
|
# device > (not encoder,goog,mime,codec) > size > fps
|
|
xmlInfo = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
|
|
|
for mime, encoder, goog in sorted(set(self.kind.values())):
|
|
for dev, build, codec, size, num, std, avg, p0, p5, p10, p20, p30, p40, p50, p60, p70, p80, p90, p95, p100 in self.data:
|
|
if self.kind[codec] != (mime, encoder, goog):
|
|
continue
|
|
|
|
if p95 > 2: # ignore measurements at or below 2fps
|
|
xmlInfo[dev][(not encoder, goog, mime, codec)][size].append(p95)
|
|
else:
|
|
print >> sys.stderr, "warning: p95 value is suspiciously low: %s" % (
|
|
nice(dict(config=dict(dev=dev, codec=codec, size=str(size), N=num),
|
|
data=dict(std=std, avg=avg, p0=p0, p5=p5, p10=p10, p20=p20, p30=p30, p40=p40,
|
|
p50=p50, p60=p60, p70=p70, p80=p80, p90=p90, p95=p95, p100=p100))))
|
|
for dev, ddata in xmlInfo.items():
|
|
outFile = '{}.media_codecs_performance.xml'.format(dev)
|
|
print >> sys.stderr, "generating", outFile
|
|
with open(outFile, "wt") as out:
|
|
for l in genXml(ddata, A=A):
|
|
out.write(l + '\n')
|
|
print l
|
|
print >> sys.stderr, "generated", outFile
|
|
|
|
def parse_fmt(self, fmt):
|
|
return self.parser._parseDict(fmt)
|
|
|
|
def parse_perf(self, a, device, build):
|
|
def rateFn(i):
|
|
if i is None:
|
|
return i
|
|
elif i == 0:
|
|
return 1e6
|
|
return 1000. / i
|
|
|
|
points = ('avg', 'min', 'p5', 'p10', 'p20', 'p30', 'p40', 'p50', 'p60', 'p70', 'p80', 'p90', 'p95', 'max')
|
|
a = dict(a)
|
|
codec = a['codec_name'] + ''
|
|
mime = a['mime_type']
|
|
size = Size(a['width'], a['height'])
|
|
if 'decode_to' in a:
|
|
fmt = self.parse_fmt(a['output_format'])
|
|
ofmt = self.parse_fmt(a['input_format'])
|
|
else:
|
|
fmt = self.parse_fmt(a['input_format'])
|
|
ofmt = self.parse_fmt(a['output_format'])
|
|
size = Size(max(fmt['width'], ofmt['width']), max(fmt['height'], ofmt['height']))
|
|
|
|
try:
|
|
prefix = 'time_avg_stats_'
|
|
if prefix + 'stdev' in a and a[prefix + 'avg']:
|
|
stdev = (a[prefix + 'stdev'] * 1e3 / a[prefix + 'avg'] ** 2)
|
|
data = ((device, build, codec, size, a[prefix + 'num'], stdev) +
|
|
tuple(rateFn(a.get(prefix + i)) for i in points))
|
|
self.data.add(data)
|
|
self.kind[codec] = (mime, 'decode_to' not in a, codec.lower().startswith('omx.google.'))
|
|
self.devices.add(data[0])
|
|
except (KeyError, ZeroDivisionError):
|
|
print >> sys.stderr, a
|
|
raise
|
|
|
|
def parse_json(self, json, device, build):
|
|
for test, results in json:
|
|
if test in ("video_encoder_performance", "video_decoder_performance"):
|
|
try:
|
|
if isinstance(results, list) and len(results[0]) and len(results[0][0]) == 2 and len(results[0][0][0]):
|
|
for result in results:
|
|
self.parse_perf(result, device, build)
|
|
else:
|
|
self.parse_perf(results, device, build)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
|
|
def parse_result(self, result):
|
|
device, build = '', ''
|
|
if not result.endswith('.zip'):
|
|
print >> sys.stderr, "cannot parse %s" % result
|
|
return
|
|
|
|
try:
|
|
with zipfile.ZipFile(result) as zip:
|
|
resultInfo, testInfos = None, []
|
|
for info in zip.infolist():
|
|
if re.search(r'/GenericDeviceInfo.deviceinfo.json$', info.filename):
|
|
resultInfo = info
|
|
elif re.search(r'/Cts(Media|Video)TestCases\.reportlog\.json$', info.filename):
|
|
testInfos.append(info)
|
|
if resultInfo:
|
|
try:
|
|
jsonFile = zip.open(resultInfo)
|
|
jsonData = json.load(jsonFile, encoding='utf-8')
|
|
device, build = jsonData['build_device'], jsonData['build_id']
|
|
except ValueError:
|
|
print >> sys.stderr, "could not parse %s" % resultInfo.filename
|
|
for info in testInfos:
|
|
jsonFile = zip.open(info)
|
|
try:
|
|
jsonData = json.load(jsonFile, encoding='utf-8', object_pairs_hook=lambda items: items)
|
|
except ValueError:
|
|
print >> sys.stderr, "cannot parse JSON in %s" % info.filename
|
|
self.parse_json(jsonData, device, build)
|
|
|
|
except zipfile.BadZipfile:
|
|
raise ValueError('bad zipfile')
|
|
|
|
|
|
P = argparse.ArgumentParser("gar_v2")
|
|
P.add_argument("--dbg", "-v", action='store_true', help="dump debug info into xml")
|
|
P.add_argument("--ignore", "-I", action='store_true', help="ignore minimum sample count")
|
|
P.add_argument("result_zip", nargs="*")
|
|
A = P.parse_args()
|
|
|
|
D = Data()
|
|
for res in A.result_zip:
|
|
D.parse_result(res)
|
|
D.summarize(A=A)
|