summary.py 15.3 KB
Newer Older
1
#!/usr/bin/env python
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
""" Format performance test results and compare metrics between test runs

Performance data is stored in the GTest log file created by performance tests. Default name is
`test_details.xml`. It can be changed with the `--gtest_output=xml:<location>/<filename>.xml` test
option. See https://github.com/opencv/opencv/wiki/HowToUsePerfTests for more details.

This script allows to compare performance data collected during separate test runs and present it in
a text, Markdown or HTML table.

### Major options

-o FMT, --output=FMT        - output format ('txt', 'html', 'markdown', 'tabs' or 'auto')
-f REGEX, --filter=REGEX    - regex to filter tests
-m NAME, --metric=NAME      - output metric
-u UNITS, --units=UNITS     - units for output values (s, ms (default), us, ns or ticks)

### Example

./summary.py -f LUT.*640 core1.xml core2.xml

Geometric mean (ms)

            Name of Test              core1  core2   core2
                                                       vs
                                                     core1
                                                   (x-factor)
LUT::OCL_LUTFixture::(640x480, 8UC1)  2.278  0.737    3.09
LUT::OCL_LUTFixture::(640x480, 32FC1) 2.622  0.805    3.26
LUT::OCL_LUTFixture::(640x480, 8UC4)  19.243 3.624    5.31
LUT::OCL_LUTFixture::(640x480, 32FC4) 21.254 4.296    4.95
LUT::SizePrm::640x480                 2.268  0.687    3.30
"""
34

35
from __future__ import print_function
36
import testlog_parser, sys, os, xml, glob, re
37 38 39
from table_formatter import *
from optparse import OptionParser

40 41 42 43 44 45 46
numeric_re = re.compile("(\d+)")
cvtype_re = re.compile("(8U|8S|16U|16S|32S|32F|64F)C(\d{1,3})")
cvtypes = { '8U': 0, '8S': 1, '16U': 2, '16S': 3, '32S': 4, '32F': 5, '64F': 6 }

convert = lambda text: int(text) if text.isdigit() else text
keyselector = lambda a: cvtype_re.sub(lambda match: " " + str(cvtypes.get(match.group(1), 7) + (int(match.group(2))-1) * 8) + " ", a)
alphanum_keyselector = lambda key: [ convert(c) for c in numeric_re.split(keyselector(key)) ]
47

48
def getSetName(tset, idx, columns, short = True):
A
Andrey Kamaev 已提交
49
    if columns and len(columns) > idx:
50 51 52 53 54 55 56
        prefix = columns[idx]
    else:
        prefix = None
    if short and prefix:
        return prefix
    name = tset[0].replace(".xml","").replace("_", "\n")
    if prefix:
57
        return prefix + "\n" + ("-"*int(len(max(prefix.split("\n"), key=len))*1.5)) + "\n" + name
58 59
    return name

60 61
if __name__ == "__main__":
    if len(sys.argv) < 2:
62
        print("Usage:\n", os.path.basename(sys.argv[0]), "<log_name1>.xml [<log_name2>.xml ...]", file=sys.stderr)
63
        exit(0)
64

65
    parser = OptionParser()
66
    parser.add_option("-o", "--output", dest="format", help="output results in text format (can be 'txt', 'html', 'markdown', 'tabs' or 'auto' - default)", metavar="FMT", default="auto")
67
    parser.add_option("-m", "--metric", dest="metric", help="output metric", metavar="NAME", default="gmean")
68
    parser.add_option("-u", "--units", dest="units", help="units for output values (s, ms (default), us, ns or ticks)", metavar="UNITS", default="ms")
69
    parser.add_option("-f", "--filter", dest="filter", help="regex to filter tests", metavar="REGEX", default=None)
70 71
    parser.add_option("", "--module", dest="module", default=None, metavar="NAME", help="module prefix for test names")
    parser.add_option("", "--columns", dest="columns", default=None, metavar="NAMES", help="comma-separated list of column aliases")
72
    parser.add_option("", "--no-relatives", action="store_false", dest="calc_relatives", default=True, help="do not output relative values")
73 74
    parser.add_option("", "--with-cycles-reduction", action="store_true", dest="calc_cr", default=False, help="output cycle reduction percentages")
    parser.add_option("", "--with-score", action="store_true", dest="calc_score", default=False, help="output automatic classification of speedups")
75
    parser.add_option("", "--progress", action="store_true", dest="progress_mode", default=False, help="enable progress mode")
76
    parser.add_option("", "--regressions", dest="regressions", default=None, metavar="LIST", help="comma-separated custom regressions map: \"[r][c]#current-#reference\" (indexes of columns are 0-based, \"r\" - reverse flag, \"c\" - color flag for base data)")
77
    parser.add_option("", "--show-all", action="store_true", dest="showall", default=False, help="also include empty and \"notrun\" lines")
78 79
    parser.add_option("", "--match", dest="match", default=None)
    parser.add_option("", "--match-replace", dest="match_replace", default="")
80
    parser.add_option("", "--regressions-only", dest="regressionsOnly", default=None, metavar="X-FACTOR", help="show only tests with performance regressions not")
81
    parser.add_option("", "--intersect-logs", dest="intersect_logs", default=False, help="show only tests present in all log files")
82
    parser.add_option("", "--show_units", action="store_true", dest="show_units", help="append units into table cells")
83
    (options, args) = parser.parse_args()
84

85 86 87
    options.generateHtml = detectHtmlOutputType(options.format)
    if options.metric not in metrix_table:
        options.metric = "gmean"
88
    if options.metric.endswith("%") or options.metric.endswith("$"):
89
        options.calc_relatives = False
90 91
        options.calc_cr = False
    if options.columns:
92
        options.columns = [s.strip().replace("\\n", "\n") for s in options.columns.split(",")]
93

94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
    if options.regressions:
        assert not options.progress_mode, 'unsupported mode'

        def parseRegressionColumn(s):
            """ Format: '[r][c]<uint>-<uint>' """
            reverse = s.startswith('r')
            if reverse:
                s = s[1:]
            addColor = s.startswith('c')
            if addColor:
                s = s[1:]
            parts = s.split('-', 1)
            link = (int(parts[0]), int(parts[1]), reverse, addColor)
            assert link[0] != link[1]
            return link

        options.regressions = [parseRegressionColumn(s) for s in options.regressions.split(',')]

112 113
    show_units = options.units if options.show_units else None

114
    # expand wildcards and filter duplicates
115 116
    files = []
    seen = set()
117 118
    for arg in args:
        if ("*" in arg) or ("?" in arg):
119 120 121
            flist = [os.path.abspath(f) for f in glob.glob(arg)]
            flist = sorted(flist, key= lambda text: str(text).replace("M", "_"))
            files.extend([ x for x in flist if x not in seen and not seen.add(x)])
122
        else:
123
            fname = os.path.abspath(arg)
A
Andrey Kamaev 已提交
124
            if fname not in seen and not seen.add(fname):
125
                files.append(fname)
126

127 128 129 130 131 132 133
    # read all passed files
    test_sets = []
    for arg in files:
        try:
            tests = testlog_parser.parseLogFile(arg)
            if options.filter:
                expr = re.compile(options.filter)
134 135 136
                tests = [t for t in tests if expr.search(str(t))]
            if options.match:
                tests = [t for t in tests if t.get("status") != "notrun"]
137 138 139 140 141 142
            if tests:
                test_sets.append((os.path.basename(arg), tests))
        except IOError as err:
            sys.stderr.write("IOError reading \"" + arg + "\" - " + str(err) + os.linesep)
        except xml.parsers.expat.ExpatError as err:
            sys.stderr.write("ExpatError reading \"" + arg + "\" - " + str(err) + os.linesep)
143

144 145 146
    if not test_sets:
        sys.stderr.write("Error: no test data found" + os.linesep)
        quit()
147

148
    setsCount = len(test_sets)
149 150 151 152 153 154 155 156 157 158 159

    if options.regressions is None:
        reference = -1 if options.progress_mode else 0
        options.regressions = [(i, reference, False, True) for i in range(1, len(test_sets))]

    for link in options.regressions:
        (i, ref, reverse, addColor) = link
        assert i >= 0 and i < setsCount
        assert ref < setsCount

    # find matches
160
    test_cases = {}
161

162 163 164 165
    name_extractor = lambda name: str(name)
    if options.match:
        reg = re.compile(options.match)
        name_extractor = lambda name: reg.sub(options.match_replace, str(name))
166

167 168
    for i in range(setsCount):
        for case in test_sets[i][1]:
169
            name = name_extractor(case)
170 171
            if options.module:
                name = options.module + "::" + name
172 173 174
            if name not in test_cases:
                test_cases[name] = [None] * setsCount
            test_cases[name][i] = case
175

176 177
    # build table
    getter = metrix_table[options.metric][1]
178 179 180
    getter_score = metrix_table["score"][1] if options.calc_score else None
    getter_p = metrix_table[options.metric + "%"][1] if options.calc_relatives else None
    getter_cr = metrix_table[options.metric + "$"][1] if options.calc_cr else None
181
    tbl = table('%s (%s)' % (metrix_table[options.metric][0], options.units), options.format)
182

183
    # header
184
    tbl.newColumn("name", "Name of Test", align = "left", cssclass = "col_name")
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
    for i in range(setsCount):
        tbl.newColumn(str(i), getSetName(test_sets[i], i, options.columns, False), align = "center")

    def addHeaderColumns(suffix, description, cssclass):
        for link in options.regressions:
            (i, ref, reverse, addColor) = link
            if reverse:
                i, ref = ref, i
            current_set = test_sets[i]
            current = getSetName(current_set, i, options.columns)
            if ref >= 0:
                reference_set = test_sets[ref]
                reference = getSetName(reference_set, ref, options.columns)
            else:
                reference = 'previous'
            tbl.newColumn(str(i) + '-' + str(ref) + suffix, '%s\nvs\n%s\n(%s)' % (current, reference, description), align='center', cssclass=cssclass)

202
    if options.calc_cr:
203
        addHeaderColumns(suffix='$', description='cycles reduction', cssclass='col_cr')
204
    if options.calc_relatives:
205
        addHeaderColumns(suffix='%', description='x-factor', cssclass='col_rel')
206
    if options.calc_score:
207
        addHeaderColumns(suffix='S', description='score', cssclass='col_name')
208

209
    # rows
210
    prevGroupName = None
211
    needNewRow = True
212
    lastRow = None
213
    for name in sorted(test_cases.keys(), key=alphanum_keyselector):
214 215
        cases = test_cases[name]
        if needNewRow:
216
            lastRow = tbl.newRow()
217 218 219
            if not options.showall:
                needNewRow = False
        tbl.newCell("name", name)
220

221 222 223 224 225 226 227
        groupName = next(c for c in cases if c).shortName()
        if groupName != prevGroupName:
            prop = lastRow.props.get("cssclass", "")
            if "firstingroup" not in prop:
                lastRow.props["cssclass"] = prop + " firstingroup"
            prevGroupName = groupName

228 229 230
        for i in range(setsCount):
            case = cases[i]
            if case is None:
231 232 233
                if options.intersect_logs:
                    needNewRow = False
                    break
234 235 236 237
                tbl.newCell(str(i), "-")
            else:
                status = case.get("status")
                if status != "run":
238
                    tbl.newCell(str(i), status, color="red")
239 240 241 242
                else:
                    val = getter(case, cases[0], options.units)
                    if val:
                        needNewRow = True
243
                    tbl.newCell(str(i), formatValue(val, options.metric, show_units), val)
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291

        if needNewRow:
            for link in options.regressions:
                (i, reference, reverse, addColor) = link
                if reverse:
                    i, reference = reference, i
                tblCellID = str(i) + '-' + str(reference)
                case = cases[i]
                if case is None:
                    if options.calc_relatives:
                        tbl.newCell(tblCellID + "%", "-")
                    if options.calc_cr:
                        tbl.newCell(tblCellID + "$", "-")
                    if options.calc_score:
                        tbl.newCell(tblCellID + "$", "-")
                else:
                    status = case.get("status")
                    if status != "run":
                        tbl.newCell(str(i), status, color="red")
                        if status != "notrun":
                            needNewRow = True
                        if options.calc_relatives:
                            tbl.newCell(tblCellID + "%", "-", color="red")
                        if options.calc_cr:
                            tbl.newCell(tblCellID + "$", "-", color="red")
                        if options.calc_score:
                            tbl.newCell(tblCellID + "S", "-", color="red")
                    else:
                        val = getter(case, cases[0], options.units)
                        def getRegression(fn):
                            if fn and val:
                                for j in reversed(range(i)) if reference < 0 else [reference]:
                                    r = cases[j]
                                    if r is not None and r.get("status") == 'run':
                                        return fn(case, r, options.units)
                        valp = getRegression(getter_p) if options.calc_relatives or options.progress_mode else None
                        valcr = getRegression(getter_cr) if options.calc_cr else None
                        val_score = getRegression(getter_score) if options.calc_score else None
                        if not valp:
                            color = None
                        elif valp > 1.05:
                            color = 'green'
                        elif valp < 0.95:
                            color = 'red'
                        else:
                            color = None
                        if addColor:
                            if not reverse:
292
                                tbl.newCell(str(i), formatValue(val, options.metric, show_units), val, color=color)
293 294 295 296
                            else:
                                r = cases[reference]
                                if r is not None and r.get("status") == 'run':
                                    val = getter(r, cases[0], options.units)
297
                                    tbl.newCell(str(reference), formatValue(val, options.metric, show_units), val, color=color)
298 299 300 301 302 303 304
                        if options.calc_relatives:
                            tbl.newCell(tblCellID + "%", formatValue(valp, "%"), valp, color=color, bold=color)
                        if options.calc_cr:
                            tbl.newCell(tblCellID + "$", formatValue(valcr, "$"), valcr, color=color, bold=color)
                        if options.calc_score:
                            tbl.newCell(tblCellID + "S", formatValue(val_score, "S"), val_score, color = color, bold = color)

305 306 307
    if not needNewRow:
        tbl.trimLastRow()

308 309
    if options.regressionsOnly:
        for r in reversed(range(len(tbl.rows))):
310 311
            for i in range(1, len(options.regressions) + 1):
                val = tbl.rows[r].cells[len(tbl.rows[r].cells) - i].value
312
                if val is not None and val < float(options.regressionsOnly):
313 314
                    break
            else:
315 316
                tbl.rows.pop(r)

317 318
    # output table
    if options.generateHtml:
319 320 321 322 323 324
        if options.format == "moinwiki":
            tbl.htmlPrintTable(sys.stdout, True)
        else:
            htmlPrintHeader(sys.stdout, "Summary report for %s tests from %s test logs" % (len(test_cases), setsCount))
            tbl.htmlPrintTable(sys.stdout)
            htmlPrintFooter(sys.stdout)
325 326
    else:
        tbl.consolePrintTable(sys.stdout)
327 328 329

    if options.regressionsOnly:
        sys.exit(len(tbl.rows))