summary.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. #!/usr/bin/env python
  2. """ Format performance test results and compare metrics between test runs
  3. Performance data is stored in the GTest log file created by performance tests. Default name is
  4. `test_details.xml`. It can be changed with the `--gtest_output=xml:<location>/<filename>.xml` test
  5. option. See https://github.com/opencv/opencv/wiki/HowToUsePerfTests for more details.
  6. This script allows to compare performance data collected during separate test runs and present it in
  7. a text, Markdown or HTML table.
  8. ### Major options
  9. -o FMT, --output=FMT - output format ('txt', 'html', 'markdown', 'tabs' or 'auto')
  10. -f REGEX, --filter=REGEX - regex to filter tests
  11. -m NAME, --metric=NAME - output metric
  12. -u UNITS, --units=UNITS - units for output values (s, ms (default), us, ns or ticks)
  13. ### Example
  14. ./summary.py -f LUT.*640 core1.xml core2.xml
  15. Geometric mean (ms)
  16. Name of Test core1 core2 core2
  17. vs
  18. core1
  19. (x-factor)
  20. LUT::OCL_LUTFixture::(640x480, 8UC1) 2.278 0.737 3.09
  21. LUT::OCL_LUTFixture::(640x480, 32FC1) 2.622 0.805 3.26
  22. LUT::OCL_LUTFixture::(640x480, 8UC4) 19.243 3.624 5.31
  23. LUT::OCL_LUTFixture::(640x480, 32FC4) 21.254 4.296 4.95
  24. LUT::SizePrm::640x480 2.268 0.687 3.30
  25. """
  26. from __future__ import print_function
  27. import testlog_parser, sys, os, xml, glob, re
  28. from table_formatter import *
  29. from optparse import OptionParser
  30. numeric_re = re.compile(r"(\d+)")
  31. cvtype_re = re.compile(r"(8U|8S|16U|16S|32S|32F|64F)C(\d{1,3})")
  32. cvtypes = { '8U': 0, '8S': 1, '16U': 2, '16S': 3, '32S': 4, '32F': 5, '64F': 6 }
  33. convert = lambda text: int(text) if text.isdigit() else text
  34. keyselector = lambda a: cvtype_re.sub(lambda match: " " + str(cvtypes.get(match.group(1), 7) + (int(match.group(2))-1) * 8) + " ", a)
  35. alphanum_keyselector = lambda key: [ convert(c) for c in numeric_re.split(keyselector(key)) ]
  36. def getSetName(tset, idx, columns, short = True):
  37. if columns and len(columns) > idx:
  38. prefix = columns[idx]
  39. else:
  40. prefix = None
  41. if short and prefix:
  42. return prefix
  43. name = tset[0].replace(".xml","").replace("_", "\n")
  44. if prefix:
  45. return prefix + "\n" + ("-"*int(len(max(prefix.split("\n"), key=len))*1.5)) + "\n" + name
  46. return name
  47. if __name__ == "__main__":
  48. if len(sys.argv) < 2:
  49. print("Usage:\n", os.path.basename(sys.argv[0]), "<log_name1>.xml [<log_name2>.xml ...]", file=sys.stderr)
  50. exit(0)
  51. parser = OptionParser()
  52. 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")
  53. parser.add_option("-m", "--metric", dest="metric", help="output metric", metavar="NAME", default="gmean")
  54. parser.add_option("-u", "--units", dest="units", help="units for output values (s, ms (default), us, ns or ticks)", metavar="UNITS", default="ms")
  55. parser.add_option("-f", "--filter", dest="filter", help="regex to filter tests", metavar="REGEX", default=None)
  56. parser.add_option("", "--module", dest="module", default=None, metavar="NAME", help="module prefix for test names")
  57. parser.add_option("", "--columns", dest="columns", default=None, metavar="NAMES", help="comma-separated list of column aliases")
  58. parser.add_option("", "--no-relatives", action="store_false", dest="calc_relatives", default=True, help="do not output relative values")
  59. parser.add_option("", "--with-cycles-reduction", action="store_true", dest="calc_cr", default=False, help="output cycle reduction percentages")
  60. parser.add_option("", "--with-score", action="store_true", dest="calc_score", default=False, help="output automatic classification of speedups")
  61. parser.add_option("", "--progress", action="store_true", dest="progress_mode", default=False, help="enable progress mode")
  62. 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)")
  63. parser.add_option("", "--show-all", action="store_true", dest="showall", default=False, help="also include empty and \"notrun\" lines")
  64. parser.add_option("", "--match", dest="match", default=None)
  65. parser.add_option("", "--match-replace", dest="match_replace", default="")
  66. parser.add_option("", "--regressions-only", dest="regressionsOnly", default=None, metavar="X-FACTOR", help="show only tests with performance regressions not")
  67. parser.add_option("", "--intersect-logs", dest="intersect_logs", default=False, help="show only tests present in all log files")
  68. parser.add_option("", "--show_units", action="store_true", dest="show_units", help="append units into table cells")
  69. (options, args) = parser.parse_args()
  70. options.generateHtml = detectHtmlOutputType(options.format)
  71. if options.metric not in metrix_table:
  72. options.metric = "gmean"
  73. if options.metric.endswith("%") or options.metric.endswith("$"):
  74. options.calc_relatives = False
  75. options.calc_cr = False
  76. if options.columns:
  77. options.columns = [s.strip().replace("\\n", "\n") for s in options.columns.split(",")]
  78. if options.regressions:
  79. assert not options.progress_mode, 'unsupported mode'
  80. def parseRegressionColumn(s):
  81. """ Format: '[r][c]<uint>-<uint>' """
  82. reverse = s.startswith('r')
  83. if reverse:
  84. s = s[1:]
  85. addColor = s.startswith('c')
  86. if addColor:
  87. s = s[1:]
  88. parts = s.split('-', 1)
  89. link = (int(parts[0]), int(parts[1]), reverse, addColor)
  90. assert link[0] != link[1]
  91. return link
  92. options.regressions = [parseRegressionColumn(s) for s in options.regressions.split(',')]
  93. show_units = options.units if options.show_units else None
  94. # expand wildcards and filter duplicates
  95. files = []
  96. seen = set()
  97. for arg in args:
  98. if ("*" in arg) or ("?" in arg):
  99. flist = [os.path.abspath(f) for f in glob.glob(arg)]
  100. flist = sorted(flist, key= lambda text: str(text).replace("M", "_"))
  101. files.extend([ x for x in flist if x not in seen and not seen.add(x)])
  102. else:
  103. fname = os.path.abspath(arg)
  104. if fname not in seen and not seen.add(fname):
  105. files.append(fname)
  106. # read all passed files
  107. test_sets = []
  108. for arg in files:
  109. try:
  110. tests = testlog_parser.parseLogFile(arg)
  111. if options.filter:
  112. expr = re.compile(options.filter)
  113. tests = [t for t in tests if expr.search(str(t))]
  114. if options.match:
  115. tests = [t for t in tests if t.get("status") != "notrun"]
  116. if tests:
  117. test_sets.append((os.path.basename(arg), tests))
  118. except IOError as err:
  119. sys.stderr.write("IOError reading \"" + arg + "\" - " + str(err) + os.linesep)
  120. except xml.parsers.expat.ExpatError as err:
  121. sys.stderr.write("ExpatError reading \"" + arg + "\" - " + str(err) + os.linesep)
  122. if not test_sets:
  123. sys.stderr.write("Error: no test data found" + os.linesep)
  124. quit()
  125. setsCount = len(test_sets)
  126. if options.regressions is None:
  127. reference = -1 if options.progress_mode else 0
  128. options.regressions = [(i, reference, False, True) for i in range(1, len(test_sets))]
  129. for link in options.regressions:
  130. (i, ref, reverse, addColor) = link
  131. assert i >= 0 and i < setsCount
  132. assert ref < setsCount
  133. # find matches
  134. test_cases = {}
  135. name_extractor = lambda name: str(name)
  136. if options.match:
  137. reg = re.compile(options.match)
  138. name_extractor = lambda name: reg.sub(options.match_replace, str(name))
  139. for i in range(setsCount):
  140. for case in test_sets[i][1]:
  141. name = name_extractor(case)
  142. if options.module:
  143. name = options.module + "::" + name
  144. if name not in test_cases:
  145. test_cases[name] = [None] * setsCount
  146. test_cases[name][i] = case
  147. # build table
  148. getter = metrix_table[options.metric][1]
  149. getter_score = metrix_table["score"][1] if options.calc_score else None
  150. getter_p = metrix_table[options.metric + "%"][1] if options.calc_relatives else None
  151. getter_cr = metrix_table[options.metric + "$"][1] if options.calc_cr else None
  152. tbl = table('%s (%s)' % (metrix_table[options.metric][0], options.units), options.format)
  153. # header
  154. tbl.newColumn("name", "Name of Test", align = "left", cssclass = "col_name")
  155. for i in range(setsCount):
  156. tbl.newColumn(str(i), getSetName(test_sets[i], i, options.columns, False), align = "center")
  157. def addHeaderColumns(suffix, description, cssclass):
  158. for link in options.regressions:
  159. (i, ref, reverse, addColor) = link
  160. if reverse:
  161. i, ref = ref, i
  162. current_set = test_sets[i]
  163. current = getSetName(current_set, i, options.columns)
  164. if ref >= 0:
  165. reference_set = test_sets[ref]
  166. reference = getSetName(reference_set, ref, options.columns)
  167. else:
  168. reference = 'previous'
  169. 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. # rows
  177. prevGroupName = None
  178. needNewRow = True
  179. lastRow = None
  180. for name in sorted(test_cases.keys(), key=alphanum_keyselector):
  181. cases = test_cases[name]
  182. if needNewRow:
  183. lastRow = tbl.newRow()
  184. if not options.showall:
  185. needNewRow = False
  186. tbl.newCell("name", name)
  187. groupName = next(c for c in cases if c).shortName()
  188. if groupName != prevGroupName:
  189. prop = lastRow.props.get("cssclass", "")
  190. if "firstingroup" not in prop:
  191. lastRow.props["cssclass"] = prop + " firstingroup"
  192. prevGroupName = groupName
  193. for i in range(setsCount):
  194. case = cases[i]
  195. if case is None:
  196. if options.intersect_logs:
  197. needNewRow = False
  198. break
  199. tbl.newCell(str(i), "-")
  200. else:
  201. status = case.get("status")
  202. if status != "run":
  203. tbl.newCell(str(i), status, color="red")
  204. else:
  205. val = getter(case, cases[0], options.units)
  206. if val:
  207. needNewRow = True
  208. tbl.newCell(str(i), formatValue(val, options.metric, show_units), val)
  209. if needNewRow:
  210. for link in options.regressions:
  211. (i, reference, reverse, addColor) = link
  212. if reverse:
  213. i, reference = reference, i
  214. tblCellID = str(i) + '-' + str(reference)
  215. case = cases[i]
  216. if case is None:
  217. if options.calc_relatives:
  218. tbl.newCell(tblCellID + "%", "-")
  219. if options.calc_cr:
  220. tbl.newCell(tblCellID + "$", "-")
  221. if options.calc_score:
  222. tbl.newCell(tblCellID + "$", "-")
  223. else:
  224. status = case.get("status")
  225. if status != "run":
  226. tbl.newCell(str(i), status, color="red")
  227. if status != "notrun":
  228. needNewRow = True
  229. if options.calc_relatives:
  230. tbl.newCell(tblCellID + "%", "-", color="red")
  231. if options.calc_cr:
  232. tbl.newCell(tblCellID + "$", "-", color="red")
  233. if options.calc_score:
  234. tbl.newCell(tblCellID + "S", "-", color="red")
  235. else:
  236. val = getter(case, cases[0], options.units)
  237. def getRegression(fn):
  238. if fn and val:
  239. for j in reversed(range(i)) if reference < 0 else [reference]:
  240. r = cases[j]
  241. if r is not None and r.get("status") == 'run':
  242. return fn(case, r, options.units)
  243. valp = getRegression(getter_p) if options.calc_relatives or options.progress_mode else None
  244. valcr = getRegression(getter_cr) if options.calc_cr else None
  245. val_score = getRegression(getter_score) if options.calc_score else None
  246. if not valp:
  247. color = None
  248. elif valp > 1.05:
  249. color = 'green'
  250. elif valp < 0.95:
  251. color = 'red'
  252. else:
  253. color = None
  254. if addColor:
  255. if not reverse:
  256. tbl.newCell(str(i), formatValue(val, options.metric, show_units), val, color=color)
  257. else:
  258. r = cases[reference]
  259. if r is not None and r.get("status") == 'run':
  260. val = getter(r, cases[0], options.units)
  261. tbl.newCell(str(reference), formatValue(val, options.metric, show_units), val, color=color)
  262. if options.calc_relatives:
  263. tbl.newCell(tblCellID + "%", formatValue(valp, "%"), valp, color=color, bold=color)
  264. if options.calc_cr:
  265. tbl.newCell(tblCellID + "$", formatValue(valcr, "$"), valcr, color=color, bold=color)
  266. if options.calc_score:
  267. tbl.newCell(tblCellID + "S", formatValue(val_score, "S"), val_score, color = color, bold = color)
  268. if not needNewRow:
  269. tbl.trimLastRow()
  270. if options.regressionsOnly:
  271. for r in reversed(range(len(tbl.rows))):
  272. for i in range(1, len(options.regressions) + 1):
  273. val = tbl.rows[r].cells[len(tbl.rows[r].cells) - i].value
  274. if val is not None and val < float(options.regressionsOnly):
  275. break
  276. else:
  277. tbl.rows.pop(r)
  278. # output table
  279. if options.generateHtml:
  280. if options.format == "moinwiki":
  281. tbl.htmlPrintTable(sys.stdout, True)
  282. else:
  283. htmlPrintHeader(sys.stdout, "Summary report for %s tests from %s test logs" % (len(test_cases), setsCount))
  284. tbl.htmlPrintTable(sys.stdout)
  285. htmlPrintFooter(sys.stdout)
  286. else:
  287. tbl.consolePrintTable(sys.stdout)
  288. if options.regressionsOnly:
  289. sys.exit(len(tbl.rows))