summary.py 13.9 KB
Newer Older
1 2
#!/usr/bin/env python

3
from __future__ import print_function
4
import testlog_parser, sys, os, xml, glob, re
5 6 7
from table_formatter import *
from optparse import OptionParser

8 9 10 11 12 13 14
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)) ]
15

16
def getSetName(tset, idx, columns, short = True):
A
Andrey Kamaev 已提交
17
    if columns and len(columns) > idx:
18 19 20 21 22 23 24
        prefix = columns[idx]
    else:
        prefix = None
    if short and prefix:
        return prefix
    name = tset[0].replace(".xml","").replace("_", "\n")
    if prefix:
25
        return prefix + "\n" + ("-"*int(len(max(prefix.split("\n"), key=len))*1.5)) + "\n" + name
26 27
    return name

28 29
if __name__ == "__main__":
    if len(sys.argv) < 2:
30
        print("Usage:\n", os.path.basename(sys.argv[0]), "<log_name1>.xml [<log_name2>.xml ...]", file=sys.stderr)
31
        exit(0)
32

33
    parser = OptionParser()
34
    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")
35
    parser.add_option("-m", "--metric", dest="metric", help="output metric", metavar="NAME", default="gmean")
36
    parser.add_option("-u", "--units", dest="units", help="units for output values (s, ms (default), us, ns or ticks)", metavar="UNITS", default="ms")
37
    parser.add_option("-f", "--filter", dest="filter", help="regex to filter tests", metavar="REGEX", default=None)
38 39
    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")
40
    parser.add_option("", "--no-relatives", action="store_false", dest="calc_relatives", default=True, help="do not output relative values")
41 42
    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")
43
    parser.add_option("", "--progress", action="store_true", dest="progress_mode", default=False, help="enable progress mode")
44
    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)")
45
    parser.add_option("", "--show-all", action="store_true", dest="showall", default=False, help="also include empty and \"notrun\" lines")
46 47
    parser.add_option("", "--match", dest="match", default=None)
    parser.add_option("", "--match-replace", dest="match_replace", default="")
48
    parser.add_option("", "--regressions-only", dest="regressionsOnly", default=None, metavar="X-FACTOR", help="show only tests with performance regressions not")
49
    parser.add_option("", "--intersect-logs", dest="intersect_logs", default=False, help="show only tests present in all log files")
50
    parser.add_option("", "--show_units", action="store_true", dest="show_units", help="append units into table cells")
51
    (options, args) = parser.parse_args()
52

53 54 55
    options.generateHtml = detectHtmlOutputType(options.format)
    if options.metric not in metrix_table:
        options.metric = "gmean"
56
    if options.metric.endswith("%") or options.metric.endswith("$"):
57
        options.calc_relatives = False
58 59
        options.calc_cr = False
    if options.columns:
60
        options.columns = [s.strip().replace("\\n", "\n") for s in options.columns.split(",")]
61

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
    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(',')]

80 81
    show_units = options.units if options.show_units else None

82
    # expand wildcards and filter duplicates
83 84
    files = []
    seen = set()
85 86
    for arg in args:
        if ("*" in arg) or ("?" in arg):
87 88 89
            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)])
90
        else:
91
            fname = os.path.abspath(arg)
A
Andrey Kamaev 已提交
92
            if fname not in seen and not seen.add(fname):
93
                files.append(fname)
94

95 96 97 98 99 100 101
    # read all passed files
    test_sets = []
    for arg in files:
        try:
            tests = testlog_parser.parseLogFile(arg)
            if options.filter:
                expr = re.compile(options.filter)
102 103 104
                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"]
105 106 107 108 109 110
            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)
111

112 113 114
    if not test_sets:
        sys.stderr.write("Error: no test data found" + os.linesep)
        quit()
115

116
    setsCount = len(test_sets)
117 118 119 120 121 122 123 124 125 126 127

    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
128
    test_cases = {}
129

130 131 132 133
    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))
134

135 136
    for i in range(setsCount):
        for case in test_sets[i][1]:
137
            name = name_extractor(case)
138 139
            if options.module:
                name = options.module + "::" + name
140 141 142
            if name not in test_cases:
                test_cases[name] = [None] * setsCount
            test_cases[name][i] = case
143

144 145
    # build table
    getter = metrix_table[options.metric][1]
146 147 148
    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
149
    tbl = table('%s (%s)' % (metrix_table[options.metric][0], options.units), options.format)
150

151
    # header
152
    tbl.newColumn("name", "Name of Test", align = "left", cssclass = "col_name")
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
    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)

170
    if options.calc_cr:
171
        addHeaderColumns(suffix='$', description='cycles reduction', cssclass='col_cr')
172
    if options.calc_relatives:
173
        addHeaderColumns(suffix='%', description='x-factor', cssclass='col_rel')
174
    if options.calc_score:
175
        addHeaderColumns(suffix='S', description='score', cssclass='col_name')
176

177
    # rows
178
    prevGroupName = None
179
    needNewRow = True
180
    lastRow = None
181
    for name in sorted(test_cases.keys(), key=alphanum_keyselector):
182 183
        cases = test_cases[name]
        if needNewRow:
184
            lastRow = tbl.newRow()
185 186 187
            if not options.showall:
                needNewRow = False
        tbl.newCell("name", name)
188

189 190 191 192 193 194 195
        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

196 197 198
        for i in range(setsCount):
            case = cases[i]
            if case is None:
199 200 201
                if options.intersect_logs:
                    needNewRow = False
                    break
202 203 204 205
                tbl.newCell(str(i), "-")
            else:
                status = case.get("status")
                if status != "run":
206
                    tbl.newCell(str(i), status, color="red")
207 208 209 210
                else:
                    val = getter(case, cases[0], options.units)
                    if val:
                        needNewRow = True
211
                    tbl.newCell(str(i), formatValue(val, options.metric, show_units), val)
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259

        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:
260
                                tbl.newCell(str(i), formatValue(val, options.metric, show_units), val, color=color)
261 262 263 264
                            else:
                                r = cases[reference]
                                if r is not None and r.get("status") == 'run':
                                    val = getter(r, cases[0], options.units)
265
                                    tbl.newCell(str(reference), formatValue(val, options.metric, show_units), val, color=color)
266 267 268 269 270 271 272
                        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)

273 274 275
    if not needNewRow:
        tbl.trimLastRow()

276 277
    if options.regressionsOnly:
        for r in reversed(range(len(tbl.rows))):
278 279
            for i in range(1, len(options.regressions) + 1):
                val = tbl.rows[r].cells[len(tbl.rows[r].cells) - i].value
280
                if val is not None and val < float(options.regressionsOnly):
281 282
                    break
            else:
283 284
                tbl.rows.pop(r)

285 286
    # output table
    if options.generateHtml:
287 288 289 290 291 292
        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)
293 294
    else:
        tbl.consolePrintTable(sys.stdout)
295 296 297

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