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')
101 speedup_style = time_style
102 good_speedup_style = xlwt.easyxf('font: color green', num_format_str='#0.00')
103 bad_speedup_style = xlwt.easyxf('font: color red', num_format_str='#0.00')
104 no_speedup_style = no_time_style
105 error_speedup_style = xlwt.easyxf('pattern: pattern solid, fore_color orange')
106 header_style = xlwt.easyxf('font: bold true; alignment: horizontal centre, vertical top, wrap True')
107 subheader_style = xlwt.easyxf('alignment: horizontal centre, vertical top')
109 class Collector(object):
110 def __init__(self, config_match_func, include_unmatched):
111 self.__config_cache = {}
112 self.config_match_func = config_match_func
113 self.include_unmatched = include_unmatched
115 self.extra_configurations = set()
117 # Format a sorted sequence of pairs as if it was a dictionary.
118 # We can't just use a dictionary instead, since we want to preserve the sorted order of the keys.
120 def __format_config_cache_key(pairs, multiline=False):
122 ('{\n' if multiline else '{') +
123 (',\n' if multiline else ', ').join(
124 (' ' if multiline else '') + repr(k) + ': ' + repr(v) for (k, v) in pairs) +
125 ('\n}\n' if multiline else '}')
128 def collect_from(self, xml_path, default_configuration):
129 run = parseLogFile(xml_path)
131 module = run.properties['module_name']
133 properties = run.properties.copy()
134 del properties['module_name']
136 props_key = tuple(sorted(properties.iteritems())) # dicts can't be keys
138 if props_key in self.__config_cache:
139 configuration = self.__config_cache[props_key]
141 configuration = self.config_match_func(properties)
143 if configuration is None:
144 if self.include_unmatched:
145 if default_configuration is not None:
146 configuration = default_configuration
148 configuration = Collector.__format_config_cache_key(props_key, multiline=True)
150 self.extra_configurations.add(configuration)
152 logging.warning('failed to match properties to a configuration: %s',
153 Collector.__format_config_cache_key(props_key))
156 same_config_props = [it[0] for it in self.__config_cache.iteritems() if it[1] == configuration]
157 if len(same_config_props) > 0:
158 logging.warning('property set %s matches the same configuration %r as property set %s',
159 Collector.__format_config_cache_key(props_key),
161 Collector.__format_config_cache_key(same_config_props[0]))
163 self.__config_cache[props_key] = configuration
165 if configuration is None: return
167 module_tests = self.tests.setdefault(module, {})
169 for test in run.tests:
170 test_results = module_tests.setdefault((test.shortName(), test.param()), {})
171 new_result = test.get("gmean") if test.status == 'run' else test.status
172 test_results[configuration] = min(
173 test_results.get(configuration), new_result,
174 key=lambda r: (1, r) if isinstance(r, numbers.Number) else
175 (2,) if r is not None else
177 ) # prefer lower result; prefer numbers to errors and errors to nothing
179 def make_match_func(matchers):
180 def match_func(properties):
181 for matcher in matchers:
182 if all(properties.get(name) == value
183 for (name, value) in matcher['properties'].iteritems()):
184 return matcher['name']
191 arg_parser = ArgumentParser(description='Build an XLS performance report.')
192 arg_parser.add_argument('sheet_dirs', nargs='+', metavar='DIR', help='directory containing perf test logs')
193 arg_parser.add_argument('-o', '--output', metavar='XLS', default='report.xls', help='name of output file')
194 arg_parser.add_argument('-c', '--config', metavar='CONF', help='global configuration file')
195 arg_parser.add_argument('--include-unmatched', action='store_true',
196 help='include results from XML files that were not recognized by configuration matchers')
197 arg_parser.add_argument('--show-times-per-pixel', action='store_true',
198 help='for tests that have an image size parameter, show per-pixel time, as well as total time')
200 args = arg_parser.parse_args()
202 logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
204 if args.config is not None:
205 with open(args.config) as global_conf_file:
206 global_conf = ast.literal_eval(global_conf_file.read())
212 for sheet_path in args.sheet_dirs:
214 with open(os.path.join(sheet_path, 'sheet.conf')) as sheet_conf_file:
215 sheet_conf = ast.literal_eval(sheet_conf_file.read())
216 except IOError as ioe:
217 if ioe.errno != errno.ENOENT: raise
219 logging.debug('no sheet.conf for %s', sheet_path)
221 sheet_conf = dict(global_conf.items() + sheet_conf.items())
223 config_names = sheet_conf.get('configurations', [])
224 config_matchers = sheet_conf.get('configuration_matchers', [])
226 collector = Collector(make_match_func(config_matchers), args.include_unmatched)
228 for root, _, filenames in os.walk(sheet_path):
229 logging.info('looking in %s', root)
230 for filename in fnmatch.filter(filenames, '*.xml'):
231 if os.path.normpath(sheet_path) == os.path.normpath(root):
234 default_conf = os.path.relpath(root, sheet_path)
235 collector.collect_from(os.path.join(root, filename), default_conf)
237 config_names.extend(sorted(collector.extra_configurations - set(config_names)))
239 sheet = wb.add_sheet(sheet_conf.get('sheet_name', os.path.basename(os.path.abspath(sheet_path))))
241 sheet_properties = sheet_conf.get('sheet_properties', [])
243 sheet.write(0, 0, 'Properties:')
246 'N/A' if len(sheet_properties) == 0 else
247 ' '.join(str(k) + '=' + repr(v) for (k, v) in sheet_properties))
249 sheet.row(2).height = 800
250 sheet.panes_frozen = True
251 sheet.remove_splits = True
253 sheet_comparisons = sheet_conf.get('comparisons', [])
259 for (w, caption) in [
262 (2000, 'Image\nwidth'),
263 (2000, 'Image\nheight'),
264 (2000, 'Data\ntype'),
265 (7500, 'Other parameters')]:
266 sheet.col(col).width = w
267 if args.show_times_per_pixel:
268 sheet.write_merge(row, row + 1, col, col, caption, header_style)
270 sheet.write(row, col, caption, header_style)
273 for config_name in config_names:
274 if args.show_times_per_pixel:
275 sheet.col(col).width = 3000
276 sheet.col(col + 1).width = 3000
277 sheet.write_merge(row, row, col, col + 1, config_name, header_style)
278 sheet.write(row + 1, col, 'total, ms', subheader_style)
279 sheet.write(row + 1, col + 1, 'per pixel, ns', subheader_style)
282 sheet.col(col).width = 4000
283 sheet.write(row, col, config_name, header_style)
286 col += 1 # blank column between configurations and comparisons
288 for comp in sheet_comparisons:
289 sheet.col(col).width = 4000
290 caption = comp['to'] + '\nvs\n' + comp['from']
291 if args.show_times_per_pixel:
292 sheet.write_merge(row, row + 1, col, col, caption, header_style)
294 sheet.write(row, col, caption, header_style)
297 row += 2 if args.show_times_per_pixel else 1
299 sheet.horz_split_pos = row
300 sheet.horz_split_first_visible = row
302 module_colors = sheet_conf.get('module_colors', {})
303 module_styles = {module: xlwt.easyxf('pattern: pattern solid, fore_color {}'.format(color))
304 for module, color in module_colors.iteritems()}
306 for module, tests in sorted(collector.tests.iteritems()):
307 for ((test, param), configs) in sorted(tests.iteritems()):
308 sheet.write(row, 0, module, module_styles.get(module, xlwt.Style.default_style))
309 sheet.write(row, 1, test)
311 param_list = param[1:-1].split(', ') if param.startswith('(') and param.endswith(')') else [param]
313 image_size = next(ifilter(re_image_size.match, param_list), None)
314 if image_size is not None:
315 (image_width, image_height) = map(int, image_size.split('x', 1))
316 sheet.write(row, 2, image_width)
317 sheet.write(row, 3, image_height)
318 del param_list[param_list.index(image_size)]
320 data_type = next(ifilter(re_data_type.match, param_list), None)
321 if data_type is not None:
322 sheet.write(row, 4, data_type)
323 del param_list[param_list.index(data_type)]
325 sheet.row(row).write(5, ' | '.join(param_list))
329 for c in config_names:
331 sheet.write(row, col, configs[c], time_style)
333 sheet.write(row, col, None, no_time_style)
335 if args.show_times_per_pixel:
336 sheet.write(row, col,
337 xlwt.Formula('{0} * 1000000 / ({1} * {2})'.format(
338 xlwt.Utils.rowcol_to_cell(row, col - 1),
339 xlwt.Utils.rowcol_to_cell(row, 2),
340 xlwt.Utils.rowcol_to_cell(row, 3)
346 col += 1 # blank column
348 for comp in sheet_comparisons:
349 cmp_from = configs.get(comp["from"])
350 cmp_to = configs.get(comp["to"])
352 if isinstance(cmp_from, numbers.Number) and isinstance(cmp_to, numbers.Number):
354 speedup = cmp_from / cmp_to
355 sheet.write(row, col, speedup, good_speedup_style if speedup > 1.1 else
356 bad_speedup_style if speedup < 0.9 else
358 except ArithmeticError as e:
359 sheet.write(row, col, None, error_speedup_style)
361 sheet.write(row, col, None, no_speedup_style)
366 if row % 1000 == 0: sheet.flush_row_data()
370 if __name__ == '__main__':