Merge pull request #1263 from abidrahmank:pyCLAHE_24
[profile/ivi/opencv.git] / modules / ts / misc / xls-report.py
1 #!/usr/bin/env python
2
3 """
4     This script can generate XLS reports from OpenCV tests' XML output files.
5
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.
9
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.
15
16     A configuration file must consist of a Python dictionary. The following keys
17     will be recognized:
18
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".
23
24     * 'configuration_matchers': [{'properties': {string: object}, 'name': string}]
25         Instructions for matching test run property sets to configuration names.
26
27         For each found XML file:
28
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
31            element deleted.
32
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.
36
37            Corollary 1: you should place more specific matchers before less specific
38            ones.
39
40            Corollary 2: an empty 'properties' dictionary matches every property set.
41
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.
45
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.
52
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.
56
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>.
62
63     * 'sheet_name': string
64         Name for the sheet. If this parameter is missing, the name of sheet's directory
65         will be used.
66
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.
70
71     Note that all keys are optional, although to get useful results, you'll want to
72     specify at least 'configurations' and 'configuration_matchers'.
73
74     Finally, run the script. Use the --help option for usage information.
75 """
76
77 from __future__ import division
78
79 import ast
80 import errno
81 import fnmatch
82 import logging
83 import numbers
84 import os, os.path
85 import re
86
87 from argparse import ArgumentParser
88 from glob import glob
89 from itertools import ifilter
90
91 import xlwt
92
93 from testlog_parser import parseLogFile
94
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)
97
98 time_style = xlwt.easyxf(num_format_str='#0.00')
99 no_time_style = xlwt.easyxf('pattern: pattern solid, fore_color gray25')
100
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')
108
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
114         self.tests = {}
115         self.extra_configurations = set()
116
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.
119     @staticmethod
120     def __format_config_cache_key(pairs, multiline=False):
121         return (
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 '}')
126         )
127
128     def collect_from(self, xml_path, default_configuration):
129         run = parseLogFile(xml_path)
130
131         module = run.properties['module_name']
132
133         properties = run.properties.copy()
134         del properties['module_name']
135
136         props_key = tuple(sorted(properties.iteritems())) # dicts can't be keys
137
138         if props_key in self.__config_cache:
139             configuration = self.__config_cache[props_key]
140         else:
141             configuration = self.config_match_func(properties)
142
143             if configuration is None:
144                 if self.include_unmatched:
145                     if default_configuration is not None:
146                         configuration = default_configuration
147                     else:
148                         configuration = Collector.__format_config_cache_key(props_key, multiline=True)
149
150                     self.extra_configurations.add(configuration)
151                 else:
152                     logging.warning('failed to match properties to a configuration: %s',
153                         Collector.__format_config_cache_key(props_key))
154
155             else:
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),
160                         configuration,
161                         Collector.__format_config_cache_key(same_config_props[0]))
162
163             self.__config_cache[props_key] = configuration
164
165         if configuration is None: return
166
167         module_tests = self.tests.setdefault(module, {})
168
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
176                             (3,)
177             ) # prefer lower result; prefer numbers to errors and errors to nothing
178
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']
185
186         return None
187
188     return match_func
189
190 def main():
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')
199
200     args = arg_parser.parse_args()
201
202     logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
203
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())
207     else:
208         global_conf = {}
209
210     wb = xlwt.Workbook()
211
212     for sheet_path in args.sheet_dirs:
213         try:
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
218             sheet_conf = {}
219             logging.debug('no sheet.conf for %s', sheet_path)
220
221         sheet_conf = dict(global_conf.items() + sheet_conf.items())
222
223         config_names = sheet_conf.get('configurations', [])
224         config_matchers = sheet_conf.get('configuration_matchers', [])
225
226         collector = Collector(make_match_func(config_matchers), args.include_unmatched)
227
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):
232                   default_conf = None
233                 else:
234                   default_conf = os.path.relpath(root, sheet_path)
235                 collector.collect_from(os.path.join(root, filename), default_conf)
236
237         config_names.extend(sorted(collector.extra_configurations - set(config_names)))
238
239         sheet = wb.add_sheet(sheet_conf.get('sheet_name', os.path.basename(os.path.abspath(sheet_path))))
240
241         sheet_properties = sheet_conf.get('sheet_properties', [])
242
243         sheet.write(0, 0, 'Properties:')
244
245         sheet.write(0, 1,
246           'N/A' if len(sheet_properties) == 0 else
247           ' '.join(str(k) + '=' + repr(v) for (k, v) in sheet_properties))
248
249         sheet.row(2).height = 800
250         sheet.panes_frozen = True
251         sheet.remove_splits = True
252
253         sheet_comparisons = sheet_conf.get('comparisons', [])
254
255         row = 2
256
257         col = 0
258
259         for (w, caption) in [
260                 (2500, 'Module'),
261                 (10000, 'Test'),
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)
269             else:
270                 sheet.write(row, col, caption, header_style)
271             col += 1
272
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)
280                 col += 2
281             else:
282                 sheet.col(col).width = 4000
283                 sheet.write(row, col, config_name, header_style)
284                 col += 1
285
286         col += 1 # blank column between configurations and comparisons
287
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)
293             else:
294                 sheet.write(row, col, caption, header_style)
295             col += 1
296
297         row += 2 if args.show_times_per_pixel else 1
298
299         sheet.horz_split_pos = row
300         sheet.horz_split_first_visible = row
301
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()}
305
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)
310
311                 param_list = param[1:-1].split(', ') if param.startswith('(') and param.endswith(')') else [param]
312
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)]
319
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)]
324
325                 sheet.row(row).write(5, ' | '.join(param_list))
326
327                 col = 6
328
329                 for c in config_names:
330                     if c in configs:
331                         sheet.write(row, col, configs[c], time_style)
332                     else:
333                         sheet.write(row, col, None, no_time_style)
334                     col += 1
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)
341                           )),
342                           time_style
343                         )
344                         col += 1
345
346                 col += 1 # blank column
347
348                 for comp in sheet_comparisons:
349                     cmp_from = configs.get(comp["from"])
350                     cmp_to = configs.get(comp["to"])
351
352                     if isinstance(cmp_from, numbers.Number) and isinstance(cmp_to, numbers.Number):
353                         try:
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
357                                                            speedup_style)
358                         except ArithmeticError as e:
359                             sheet.write(row, col, None, error_speedup_style)
360                     else:
361                         sheet.write(row, col, None, no_speedup_style)
362
363                     col += 1
364
365                 row += 1
366                 if row % 1000 == 0: sheet.flush_row_data()
367
368     wb.save(args.output)
369
370 if __name__ == '__main__':
371     main()