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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

195 196 197
        for i in range(setsCount):
            case = cases[i]
            if case is None:
198 199 200
                if options.intersect_logs:
                    needNewRow = False
                    break
201 202 203 204
                tbl.newCell(str(i), "-")
            else:
                status = case.get("status")
                if status != "run":
205
                    tbl.newCell(str(i), status, color="red")
206 207 208 209
                else:
                    val = getter(case, cases[0], options.units)
                    if val:
                        needNewRow = True
210
                    tbl.newCell(str(i), formatValue(val, options.metric, show_units), val)
211 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

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

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

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

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

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