• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2"""Utility to benchmark the generated source files"""
3
4# Copyright Abel Sinkovics (abel@sinkovics.hu) 2016.
5# Distributed under the Boost Software License, Version 1.0.
6#    (See accompanying file LICENSE_1_0.txt or copy at
7#          http://www.boost.org/LICENSE_1_0.txt)
8
9import argparse
10import os
11import subprocess
12import json
13import math
14import platform
15import matplotlib
16import random
17import re
18import time
19import psutil
20import PIL
21
22matplotlib.use('Agg')
23import matplotlib.pyplot  # pylint:disable=I0011,C0411,C0412,C0413
24
25
26def benchmark_command(cmd, progress):
27    """Benchmark one command execution"""
28    full_cmd = '/usr/bin/time --format="%U %M" {0}'.format(cmd)
29    print '{0:6.2f}% Running {1}'.format(100.0 * progress, full_cmd)
30    (_, err) = subprocess.Popen(
31        ['/bin/sh', '-c', full_cmd],
32        stdin=subprocess.PIPE,
33        stdout=subprocess.PIPE,
34        stderr=subprocess.PIPE
35    ).communicate('')
36
37    values = err.strip().split(' ')
38    if len(values) == 2:
39        try:
40            return (float(values[0]), float(values[1]))
41        except:  # pylint:disable=I0011,W0702
42            pass  # Handled by the code after the "if"
43
44    print err
45    raise Exception('Error during benchmarking')
46
47
48def benchmark_file(
49        filename, compiler, include_dirs, (progress_from, progress_to),
50        iter_count, extra_flags = ''):
51    """Benchmark one file"""
52    time_sum = 0
53    mem_sum = 0
54    for nth_run in xrange(0, iter_count):
55        (time_spent, mem_used) = benchmark_command(
56            '{0} -std=c++11 {1} -c {2} {3}'.format(
57                compiler,
58                ' '.join('-I{0}'.format(i) for i in include_dirs),
59                filename,
60                extra_flags
61            ),
62            (
63                progress_to * nth_run + progress_from * (iter_count - nth_run)
64            ) / iter_count
65        )
66        os.remove(os.path.splitext(os.path.basename(filename))[0] + '.o')
67        time_sum = time_sum + time_spent
68        mem_sum = mem_sum + mem_used
69
70    return {
71        "time": time_sum / iter_count,
72        "memory": mem_sum / (iter_count * 1024)
73    }
74
75
76def compiler_info(compiler):
77    """Determine the name + version of the compiler"""
78    (out, err) = subprocess.Popen(
79        ['/bin/sh', '-c', '{0} -v'.format(compiler)],
80        stdin=subprocess.PIPE,
81        stdout=subprocess.PIPE,
82        stderr=subprocess.PIPE
83    ).communicate('')
84
85    gcc_clang = re.compile('(gcc|clang) version ([0-9]+(\\.[0-9]+)*)')
86
87    for line in (out + err).split('\n'):
88        mtch = gcc_clang.search(line)
89        if mtch:
90            return mtch.group(1) + ' ' + mtch.group(2)
91
92    return compiler
93
94
95def string_char(char):
96    """Turn the character into one that can be part of a filename"""
97    return '_' if char in [' ', '~', '(', ')', '/', '\\'] else char
98
99
100def make_filename(string):
101    """Turn the string into a filename"""
102    return ''.join(string_char(c) for c in string)
103
104
105def files_in_dir(path, extension):
106    """Enumartes the files in path with the given extension"""
107    ends = '.{0}'.format(extension)
108    return (f for f in os.listdir(path) if f.endswith(ends))
109
110
111def format_time(seconds):
112    """Format a duration"""
113    minute = 60
114    hour = minute * 60
115    day = hour * 24
116    week = day * 7
117
118    result = []
119    for name, dur in [
120            ('week', week), ('day', day), ('hour', hour),
121            ('minute', minute), ('second', 1)
122    ]:
123        if seconds > dur:
124            value = seconds // dur
125            result.append(
126                '{0} {1}{2}'.format(int(value), name, 's' if value > 1 else '')
127            )
128            seconds = seconds % dur
129    return ' '.join(result)
130
131
132def benchmark(src_dir, compiler, include_dirs, iter_count):
133    """Do the benchmarking"""
134
135    files = list(files_in_dir(src_dir, 'cpp'))
136    random.shuffle(files)
137    has_string_templates = True
138    string_template_file_cnt = sum(1 for file in files if 'bmp' in file)
139    file_count = len(files) + string_template_file_cnt
140
141    started_at = time.time()
142    result = {}
143    for filename in files:
144        progress = len(result)
145        result[filename] = benchmark_file(
146            os.path.join(src_dir, filename),
147            compiler,
148            include_dirs,
149            (float(progress) / file_count, float(progress + 1) / file_count),
150            iter_count
151        )
152        if 'bmp' in filename and has_string_templates:
153            try:
154                temp_result = benchmark_file(
155                    os.path.join(src_dir, filename),
156                    compiler,
157                    include_dirs,
158                    (float(progress + 1) / file_count, float(progress + 2) / file_count),
159                    iter_count,
160                    '-Xclang -fstring-literal-templates'
161                )
162                result[filename.replace('bmp', 'slt')] = temp_result
163            except:
164                has_string_templates = False
165                file_count -= string_template_file_cnt
166                print 'Stopping the benchmarking of string literal templates'
167
168        elapsed = time.time() - started_at
169        total = float(file_count * elapsed) / len(result)
170        print 'Elapsed time: {0}, Remaining time: {1}'.format(
171            format_time(elapsed),
172            format_time(total - elapsed)
173        )
174    return result
175
176
177def plot(values, mode_names, title, (xlabel, ylabel), out_file):
178    """Plot a diagram"""
179    matplotlib.pyplot.clf()
180    for mode, mode_name in mode_names.iteritems():
181        vals = values[mode]
182        matplotlib.pyplot.plot(
183            [x for x, _ in vals],
184            [y for _, y in vals],
185            label=mode_name
186        )
187    matplotlib.pyplot.title(title)
188    matplotlib.pyplot.xlabel(xlabel)
189    matplotlib.pyplot.ylabel(ylabel)
190    if len(mode_names) > 1:
191        matplotlib.pyplot.legend()
192    matplotlib.pyplot.savefig(out_file)
193
194
195def mkdir_p(path):
196    """mkdir -p path"""
197    try:
198        os.makedirs(path)
199    except OSError:
200        pass
201
202
203def configs_in(src_dir):
204    """Enumerate all configs in src_dir"""
205    for filename in files_in_dir(src_dir, 'json'):
206        with open(os.path.join(src_dir, filename), 'rb') as in_f:
207            yield json.load(in_f)
208
209
210def byte_to_gb(byte):
211    """Convert bytes to GB"""
212    return byte / (1024.0 * 1024 * 1024)
213
214
215def join_images(img_files, out_file):
216    """Join the list of images into the out file"""
217    images = [PIL.Image.open(f) for f in img_files]
218    joined = PIL.Image.new(
219        'RGB',
220        (sum(i.size[0] for i in images), max(i.size[1] for i in images))
221    )
222    left = 0
223    for img in images:
224        joined.paste(im=img, box=(left, 0))
225        left = left + img.size[0]
226    joined.save(out_file)
227
228
229def plot_temp_diagrams(config, results, temp_dir):
230    """Plot temporary diagrams"""
231    display_name = {
232        'time': 'Compilation time (s)',
233        'memory': 'Compiler memory usage (MB)',
234    }
235
236    files = config['files']
237    img_files = []
238
239    if any('slt' in result for result in results) and 'bmp' in files.values()[0]:
240        config['modes']['slt'] = 'Using BOOST_METAPARSE_STRING with string literal templates'
241        for f in files.values():
242            f['slt'] = f['bmp'].replace('bmp', 'slt')
243
244    for measured in ['time', 'memory']:
245        mpts = sorted(int(k) for k in files.keys())
246        img_files.append(os.path.join(temp_dir, '_{0}.png'.format(measured)))
247        plot(
248            {
249                m: [(x, results[files[str(x)][m]][measured]) for x in mpts]
250                for m in config['modes'].keys()
251            },
252            config['modes'],
253            display_name[measured],
254            (config['x_axis_label'], display_name[measured]),
255            img_files[-1]
256        )
257    return img_files
258
259
260def plot_diagram(config, results, images_dir, out_filename):
261    """Plot one diagram"""
262    img_files = plot_temp_diagrams(config, results, images_dir)
263    join_images(img_files, out_filename)
264    for img_file in img_files:
265        os.remove(img_file)
266
267
268def plot_diagrams(results, configs, compiler, out_dir):
269    """Plot all diagrams specified by the configs"""
270    compiler_fn = make_filename(compiler)
271    total = psutil.virtual_memory().total  # pylint:disable=I0011,E1101
272    memory = int(math.ceil(byte_to_gb(total)))
273
274    images_dir = os.path.join(out_dir, 'images')
275
276    for config in configs:
277        out_prefix = '{0}_{1}'.format(config['name'], compiler_fn)
278
279        plot_diagram(
280            config,
281            results,
282            images_dir,
283            os.path.join(images_dir, '{0}.png'.format(out_prefix))
284        )
285
286        with open(
287            os.path.join(out_dir, '{0}.qbk'.format(out_prefix)),
288            'wb'
289        ) as out_f:
290            qbk_content = """{0}
291Measured on a {2} host with {3} GB memory. Compiler used: {4}.
292
293[$images/metaparse/{1}.png [width 100%]]
294""".format(config['desc'], out_prefix, platform.platform(), memory, compiler)
295            out_f.write(qbk_content)
296
297
298def main():
299    """The main function of the script"""
300    desc = 'Benchmark the files generated by generate.py'
301    parser = argparse.ArgumentParser(description=desc)
302    parser.add_argument(
303        '--src',
304        dest='src_dir',
305        default='generated',
306        help='The directory containing the sources to benchmark'
307    )
308    parser.add_argument(
309        '--out',
310        dest='out_dir',
311        default='../../doc',
312        help='The output directory'
313    )
314    parser.add_argument(
315        '--include',
316        dest='include',
317        default='include',
318        help='The directory containing the headeres for the benchmark'
319    )
320    parser.add_argument(
321        '--boost_headers',
322        dest='boost_headers',
323        default='../../../..',
324        help='The directory containing the Boost headers (the boost directory)'
325    )
326    parser.add_argument(
327        '--compiler',
328        dest='compiler',
329        default='g++',
330        help='The compiler to do the benchmark with'
331    )
332    parser.add_argument(
333        '--repeat_count',
334        dest='repeat_count',
335        type=int,
336        default=5,
337        help='How many times a measurement should be repeated.'
338    )
339
340    args = parser.parse_args()
341
342    compiler = compiler_info(args.compiler)
343    results = benchmark(
344        args.src_dir,
345        args.compiler,
346        [args.include, args.boost_headers],
347        args.repeat_count
348    )
349
350    plot_diagrams(results, configs_in(args.src_dir), compiler, args.out_dir)
351
352
353if __name__ == '__main__':
354    main()
355