benchmark.py 10 KB

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