4 This script can generate XLS reports from OpenCV tests' XML output files.
6 To use it, first, create a directory for each machine you ran tests on.
7 Each such directory will become a sheet in the report. Put each XML file
8 into the corresponding directory.
10 Then, create your configuration file(s). You can have a global configuration
11 file (specified with the -c option), and per-sheet configuration files, which
12 must be called sheet.conf and placed in the directory corresponding to the sheet.
13 The settings in the per-sheet configuration file will override those in the
14 global configuration file, if both are present.
16 A configuration file must consist of a Python dictionary. The following keys
19 * 'comparisons': [{'from': string, 'to': string}]
20 List of configurations to compare performance between. For each item,
21 the sheet will have a column showing speedup from configuration named
22 'from' to configuration named "to".
24 * 'configuration_matchers': [{'properties': {string: object}, 'name': string}]
25 Instructions for matching test run property sets to configuration names.
27 For each found XML file:
29 1) All attributes of the root element starting with the prefix 'cv_' are
30 placed in a dictionary, with the cv_ prefix stripped and the cv_module_name
33 2) The first matcher for which the XML's file property set contains the same
34 keys with equal values as its 'properties' dictionary is searched for.
35 A missing property can be matched by using None as the value.
37 Corollary 1: you should place more specific matchers before less specific
40 Corollary 2: an empty 'properties' dictionary matches every property set.
42 3) If a matching matcher is found, its 'name' string is presumed to be the name
43 of the configuration the XML file corresponds to. A warning is printed if
44 two different property sets match to the same configuration name.
46 4) If a such a matcher isn't found, if --include-unmatched was specified, the
47 configuration name is assumed to be the relative path from the sheet's
48 directory to the XML file's containing directory. If the XML file isinstance
49 directly inside the sheet's directory, the configuration name is instead
50 a dump of all its properties. If --include-unmatched wasn't specified,
51 the XML file is ignored and a warning is printed.
53 * 'configurations': [string]
54 List of names for compile-time and runtime configurations of OpenCV.
55 Each item will correspond to a column of the sheet.
57 * 'module_colors': {string: string}
58 Mapping from module name to color name. In the sheet, cells containing module
59 names from this mapping will be colored with the corresponding color. You can
60 find the list of available colors here:
61 <http://www.simplistix.co.uk/presentations/python-excel.pdf>.
63 * 'sheet_name': string
64 Name for the sheet. If this parameter is missing, the name of sheet's directory
67 * 'sheet_properties': [(string, string)]
68 List of arbitrary (key, value) pairs that somehow describe the sheet. Will be
69 dumped into the first row of the sheet in string form.
71 Note that all keys are optional, although to get useful results, you'll want to
72 specify at least 'configurations' and 'configuration_matchers'.
74 Finally, run the script. Use the --help option for usage information.
77 from __future__ import division
87 from argparse import ArgumentParser
89 from itertools import ifilter
93 from testlog_parser import parseLogFile
95 re_image_size = re.compile(r'^ \d+ x \d+$', re.VERBOSE)
96 re_data_type = re.compile(r'^ (?: 8 | 16 | 32 | 64 ) [USF] C [1234] $', re.VERBOSE)
98 time_style = xlwt.easyxf(num_format_str='#0.00')
99 no_time_style = xlwt.easyxf('pattern: pattern solid, fore_color gray25')
100 failed_style = xlwt.easyxf('pattern: pattern solid, fore_color red')
101 noimpl_style = xlwt.easyxf('pattern: pattern solid, fore_color orange')
102 style_dict = {"failed": failed_style, "noimpl":noimpl_style}
104 speedup_style = time_style
105 good_speedup_style = xlwt.easyxf('font: color green', num_format_str='#0.00')
106 bad_speedup_style = xlwt.easyxf('font: color red', num_format_str='#0.00')
107 no_speedup_style = no_time_style
108 error_speedup_style = xlwt.easyxf('pattern: pattern solid, fore_color orange')
109 header_style = xlwt.easyxf('font: bold true; alignment: horizontal centre, vertical top, wrap True')
110 subheader_style = xlwt.easyxf('alignment: horizontal centre, vertical top')
112 class Collector(object):
113 def __init__(self, config_match_func, include_unmatched):
114 self.__config_cache = {}
115 self.config_match_func = config_match_func
116 self.include_unmatched = include_unmatched
118 self.extra_configurations = set()
120 # Format a sorted sequence of pairs as if it was a dictionary.
121 # We can't just use a dictionary instead, since we want to preserve the sorted order of the keys.
123 def __format_config_cache_key(pairs, multiline=False):
125 ('{\n' if multiline else '{') +
126 (',\n' if multiline else ', ').join(
127 (' ' if multiline else '') + repr(k) + ': ' + repr(v) for (k, v) in pairs) +
128 ('\n}\n' if multiline else '}')
131 def collect_from(self, xml_path, default_configuration):
132 run = parseLogFile(xml_path)
134 module = run.properties['module_name']
136 properties = run.properties.copy()
137 del properties['module_name']
139 props_key = tuple(sorted(properties.iteritems())) # dicts can't be keys
141 if props_key in self.__config_cache:
142 configuration = self.__config_cache[props_key]
144 configuration = self.config_match_func(properties)
146 if configuration is None:
147 if self.include_unmatched:
148 if default_configuration is not None:
149 configuration = default_configuration
151 configuration = Collector.__format_config_cache_key(props_key, multiline=True)
153 self.extra_configurations.add(configuration)
155 logging.warning('failed to match properties to a configuration: %s',
156 Collector.__format_config_cache_key(props_key))
159 same_config_props = [it[0] for it in self.__config_cache.iteritems() if it[1] == configuration]
160 if len(same_config_props) > 0:
161 logging.warning('property set %s matches the same configuration %r as property set %s',
162 Collector.__format_config_cache_key(props_key),
164 Collector.__format_config_cache_key(same_config_props[0]))
166 self.__config_cache[props_key] = configuration
168 if configuration is None: return
170 module_tests = self.tests.setdefault(module, {})
172 for test in run.tests:
173 test_results = module_tests.setdefault((test.shortName(), test.param()), {})
174 new_result = test.get("gmean") if test.status == 'run' else test.status
175 test_results[configuration] = min(
176 test_results.get(configuration), new_result,
177 key=lambda r: (1, r) if isinstance(r, numbers.Number) else
178 (2,) if r is not None else
180 ) # prefer lower result; prefer numbers to errors and errors to nothing
182 def make_match_func(matchers):
183 def match_func(properties):
184 for matcher in matchers:
185 if all(properties.get(name) == value
186 for (name, value) in matcher['properties'].iteritems()):
187 return matcher['name']
194 arg_parser = ArgumentParser(description='Build an XLS performance report.')
195 arg_parser.add_argument('sheet_dirs', nargs='+', metavar='DIR', help='directory containing perf test logs')
196 arg_parser.add_argument('-o', '--output', metavar='XLS', default='report.xls', help='name of output file')
197 arg_parser.add_argument('-c', '--config', metavar='CONF', help='global configuration file')
198 arg_parser.add_argument('--include-unmatched', action='store_true',
199 help='include results from XML files that were not recognized by configuration matchers')
200 arg_parser.add_argument('--show-times-per-pixel', action='store_true',
201 help='for tests that have an image size parameter, show per-pixel time, as well as total time')
203 args = arg_parser.parse_args()
205 logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
207 if args.config is not None:
208 with open(args.config) as global_conf_file:
209 global_conf = ast.literal_eval(global_conf_file.read())
215 for sheet_path in args.sheet_dirs:
217 with open(os.path.join(sheet_path, 'sheet.conf')) as sheet_conf_file:
218 sheet_conf = ast.literal_eval(sheet_conf_file.read())
219 except IOError as ioe:
220 if ioe.errno != errno.ENOENT: raise
222 logging.debug('no sheet.conf for %s', sheet_path)
224 sheet_conf = dict(global_conf.items() + sheet_conf.items())
226 config_names = sheet_conf.get('configurations', [])
227 config_matchers = sheet_conf.get('configuration_matchers', [])
229 collector = Collector(make_match_func(config_matchers), args.include_unmatched)
231 for root, _, filenames in os.walk(sheet_path):
232 logging.info('looking in %s', root)
233 for filename in fnmatch.filter(filenames, '*.xml'):
234 if os.path.normpath(sheet_path) == os.path.normpath(root):
237 default_conf = os.path.relpath(root, sheet_path)
238 collector.collect_from(os.path.join(root, filename), default_conf)
240 config_names.extend(sorted(collector.extra_configurations - set(config_names)))
242 sheet = wb.add_sheet(sheet_conf.get('sheet_name', os.path.basename(os.path.abspath(sheet_path))))
244 sheet_properties = sheet_conf.get('sheet_properties', [])
246 sheet.write(0, 0, 'Properties:')
249 'N/A' if len(sheet_properties) == 0 else
250 ' '.join(str(k) + '=' + repr(v) for (k, v) in sheet_properties))
252 sheet.row(2).height = 800
253 sheet.panes_frozen = True
254 sheet.remove_splits = True
256 sheet_comparisons = sheet_conf.get('comparisons', [])
262 for (w, caption) in [
265 (2000, 'Image\nwidth'),
266 (2000, 'Image\nheight'),
267 (2000, 'Data\ntype'),
268 (7500, 'Other parameters')]:
269 sheet.col(col).width = w
270 if args.show_times_per_pixel:
271 sheet.write_merge(row, row + 1, col, col, caption, header_style)
273 sheet.write(row, col, caption, header_style)
276 for config_name in config_names:
277 if args.show_times_per_pixel:
278 sheet.col(col).width = 3000
279 sheet.col(col + 1).width = 3000
280 sheet.write_merge(row, row, col, col + 1, config_name, header_style)
281 sheet.write(row + 1, col, 'total, ms', subheader_style)
282 sheet.write(row + 1, col + 1, 'per pixel, ns', subheader_style)
285 sheet.col(col).width = 4000
286 sheet.write(row, col, config_name, header_style)
289 col += 1 # blank column between configurations and comparisons
291 for comp in sheet_comparisons:
292 sheet.col(col).width = 4000
293 caption = comp['to'] + '\nvs\n' + comp['from']
294 if args.show_times_per_pixel:
295 sheet.write_merge(row, row + 1, col, col, caption, header_style)
297 sheet.write(row, col, caption, header_style)
300 row += 2 if args.show_times_per_pixel else 1
302 sheet.horz_split_pos = row
303 sheet.horz_split_first_visible = row
305 module_colors = sheet_conf.get('module_colors', {})
306 module_styles = {module: xlwt.easyxf('pattern: pattern solid, fore_color {}'.format(color))
307 for module, color in module_colors.iteritems()}
309 for module, tests in sorted(collector.tests.iteritems()):
310 for ((test, param), configs) in sorted(tests.iteritems()):
311 sheet.write(row, 0, module, module_styles.get(module, xlwt.Style.default_style))
312 sheet.write(row, 1, test)
314 param_list = param[1:-1].split(', ') if param.startswith('(') and param.endswith(')') else [param]
316 image_size = next(ifilter(re_image_size.match, param_list), None)
317 if image_size is not None:
318 (image_width, image_height) = map(int, image_size.split('x', 1))
319 sheet.write(row, 2, image_width)
320 sheet.write(row, 3, image_height)
321 del param_list[param_list.index(image_size)]
323 data_type = next(ifilter(re_data_type.match, param_list), None)
324 if data_type is not None:
325 sheet.write(row, 4, data_type)
326 del param_list[param_list.index(data_type)]
328 sheet.row(row).write(5, ' | '.join(param_list))
332 for c in config_names:
334 sheet.write(row, col, configs[c], style_dict.get(configs[c], time_style))
336 sheet.write(row, col, None, no_time_style)
338 if args.show_times_per_pixel:
339 sheet.write(row, col,
340 xlwt.Formula('{0} * 1000000 / ({1} * {2})'.format(
341 xlwt.Utils.rowcol_to_cell(row, col - 1),
342 xlwt.Utils.rowcol_to_cell(row, 2),
343 xlwt.Utils.rowcol_to_cell(row, 3)
349 col += 1 # blank column
351 for comp in sheet_comparisons:
352 cmp_from = configs.get(comp["from"])
353 cmp_to = configs.get(comp["to"])
355 if isinstance(cmp_from, numbers.Number) and isinstance(cmp_to, numbers.Number):
357 speedup = cmp_from / cmp_to
358 sheet.write(row, col, speedup, good_speedup_style if speedup > 1.1 else
359 bad_speedup_style if speedup < 0.9 else
361 except ArithmeticError as e:
362 sheet.write(row, col, None, error_speedup_style)
364 sheet.write(row, col, None, no_speedup_style)
369 if row % 1000 == 0: sheet.flush_row_data()
373 if __name__ == '__main__':