run_tests.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. #!/usr/bin/python
  2. import sys
  3. import os
  4. import argparse
  5. import subprocess
  6. import multiprocessing
  7. import json
  8. import pprint
  9. def take_first(value):
  10. if type(value) is tuple or type(value) is list:
  11. return value[0]
  12. return value
  13. def take_order(value, order):
  14. for i in range(len(order)):
  15. if value == order[i]:
  16. return i
  17. return len(order)
  18. def take_map(value, keys, vals):
  19. if len(keys) != len(vals):
  20. raise RuntimeError("Length of keys and vals must match")
  21. for i in range(len(keys)):
  22. if value == keys[i]:
  23. return vals[i]
  24. return value
  25. def take_best(values, key, compare, reverse=False):
  26. if 0 == len(values):
  27. return []
  28. values = sorted(values, key=key, reverse=reverse)
  29. for i in range(1, len(values)):
  30. if not compare(values[0], values[i]):
  31. return values[0:i]
  32. return values
  33. def take_plural(count, base, suffix):
  34. if 1 == abs(count):
  35. return base
  36. return base + suffix
  37. def take_threads_variants():
  38. # single thread
  39. variants = [1]
  40. cpus = multiprocessing.cpu_count()
  41. if 1 > cpus:
  42. cpus = 1
  43. # load all CPUs
  44. if 1 < cpus:
  45. variants.append(cpus)
  46. # overcommit
  47. variants.append(2 * cpus)
  48. return variants
  49. def _cmp_percentage(p, key, a, b):
  50. if key(a) == key(b):
  51. return True
  52. if p >= abs((key(a) - key(b)) / float(key(a))):
  53. return True
  54. return False
  55. def cmp_percentage(p, key):
  56. return lambda a, b: _cmp_percentage(p, key, a, b)
  57. def translate_test(test):
  58. name = take_first(test)
  59. if "call_site_size.str" == name:
  60. return 1000, "Call site size: string"
  61. if "call_site_size.fmti" == name:
  62. return 2000, "Call site size: 3 integers"
  63. if "executable_size.m1" == name:
  64. return 3000, "Executable size: 1 module"
  65. if "compile_time" == name:
  66. return 4000, "Module compile time"
  67. if "link_time" == name:
  68. return 5000, "Executable link time"
  69. if "speed" == name:
  70. threads = test[1]
  71. mode = test[2]
  72. mode_keys = ["str", "fmti", "str-off", "slowf-off"]
  73. mode_vals = ["string", "3 integers", "string, off", "slow function, off"]
  74. tr_mode = take_map(mode, mode_keys, mode_vals)
  75. order = 10 * threads + take_order(mode, mode_keys)
  76. return 6000 + order, "Speed: %i %s, %s" % (threads, take_plural(threads, "thread", "s"), tr_mode)
  77. if type(test) is tuple or type(test) is list:
  78. return 31416, ", ".join(test)
  79. return 27183, test
  80. def translate_subj(subj):
  81. if "zf_log_n" == subj:
  82. return 31416, "zf_log"
  83. if "easylog" == subj:
  84. return 31416, "Easylogging++"
  85. return 31416, subj
  86. def translation_sort_key(v):
  87. return "[%04i] %s" % (v[0], v[1])
  88. def translation_value(v):
  89. return v[1]
  90. class data_cell:
  91. def __init__(self):
  92. self.best = False
  93. def set_best(self, best=True):
  94. self.best = best
  95. def ifbest(self, a, b):
  96. if hasattr(self, "best") and self.best:
  97. return a
  98. return b
  99. class data_str(data_cell):
  100. def __init__(self, value):
  101. if type(value) is not str:
  102. raise RuntimeError("Not a string")
  103. self.value = value
  104. def __str__(self):
  105. return self.value
  106. def __repr__(self):
  107. return repr(self.value)
  108. class data_bytes(data_cell):
  109. def __init__(self, value):
  110. if type(value) is not int:
  111. raise RuntimeError("Not an int")
  112. self.value = value
  113. def __str__(self):
  114. if self.value < 1024:
  115. return "%i B" % (self.value)
  116. if self.value < 1024 * 1024:
  117. return "%.2f KB" % (self.value / 1024.0)
  118. return "%.2f MB" % (self.value / 1024.0 / 1024.0)
  119. def __repr__(self):
  120. return repr(self.value)
  121. class data_seconds(data_cell):
  122. def __init__(self, value):
  123. if type(value) is not int and type(value) is not float:
  124. raise RuntimeError("Not an int or float")
  125. self.value = value
  126. def __str__(self):
  127. return "%.3f sec" % (self.value)
  128. def __repr__(self):
  129. return repr(self.value)
  130. class data_freq(data_cell):
  131. def __init__(self, count, seconds):
  132. if type(count) is not int and type(count) is not float:
  133. raise RuntimeError("Not an int or float")
  134. if type(seconds) is not int and type(seconds) is not float:
  135. raise RuntimeError("Not an int or float")
  136. self.count = count
  137. self.seconds = seconds
  138. def __str__(self):
  139. return "{:,}".format(self.count / self.seconds)
  140. def __repr__(self):
  141. return repr((self.count, self.seconds))
  142. def freq(self):
  143. return self.count / self.seconds
  144. def get_table_data(result):
  145. # collect all tests
  146. tests = result.keys()
  147. tests = sorted(tests, key=lambda x: translation_sort_key(translate_test(x)))
  148. # collect all subjects
  149. subjs = set()
  150. for test in tests:
  151. subjs.update(result[test].keys())
  152. subjs = sorted(subjs, key=lambda x: translation_sort_key(translate_subj(x)))
  153. # create table
  154. rows = len(tests) + 1
  155. cols = len(subjs) + 1
  156. table = [[None for _ in range(cols)] for _ in range(rows)]
  157. # put names and captions
  158. for i in range(1, rows):
  159. table[i][0] = data_str(translation_value(translate_test(tests[i - 1])))
  160. for j in range(1, cols):
  161. table[0][j] = data_str(translation_value(translate_subj(subjs[j - 1])))
  162. # put data
  163. for i in range(1, rows):
  164. for j in range(1, cols):
  165. test = tests[i - 1]
  166. subj = subjs[j - 1]
  167. if subj in result[test]:
  168. table[i][j] = result[test][subj]
  169. # gen cells content
  170. for i in range(0, rows):
  171. for j in range(0, cols):
  172. value = table[i][j]
  173. if value is None:
  174. value = data_str("")
  175. elif not isinstance(value, data_cell):
  176. raise RuntimeError("Value \"%s\" is of unsupported type \"%s\"" % (value, type(value)))
  177. table[i][j] = value
  178. # find cols width
  179. widths = [0 for _ in range(cols)]
  180. for j in range(0, cols):
  181. for i in range(0, rows):
  182. s = str(table[i][j])
  183. if widths[j] < len(s):
  184. widths[j] = len(s)
  185. return table, rows, cols, widths
  186. def gen_table_ascii(result):
  187. table, rows, cols, widths = get_table_data(result)
  188. # apply cell format
  189. margin_norm = (" ", " ")
  190. margin_best = ("*", " ")
  191. margins = max(map(len, margin_norm)) + max(map(len, margin_norm))
  192. for i in range(1, rows):
  193. table[i][0] = str(table[i][0]).ljust(widths[0]).join(margin_norm)
  194. for j in range(0, cols):
  195. table[0][j] = str(table[0][j]).center(widths[j]).join(margin_norm)
  196. for i in range(1, rows):
  197. for j in range(1, cols):
  198. data = table[i][j]
  199. margin = data.ifbest(margin_best, margin_norm)
  200. table[i][j] = str(data).rjust(widths[j]).join(margin)
  201. # draw chart
  202. line = "+" + "-" * (sum(widths) + (margins + 1) * len(widths) - 1) + "+\n"
  203. chart = line
  204. for row in table:
  205. chart += "|"
  206. for cell in row:
  207. chart += "%s|" % (cell)
  208. chart += "\n" + line
  209. return chart
  210. def gen_table_markdown(result):
  211. table, rows, cols, widths = get_table_data(result)
  212. # apply cell format
  213. margin_norm = (" ", " ")
  214. margin_best = ("**", "**")
  215. margins = max(map(len, margin_norm)) + max(map(len, margin_norm))
  216. for i in range(1, rows):
  217. table[i][0] = str(table[i][0]).ljust(widths[0]).join(margin_norm)
  218. for j in range(0, cols):
  219. table[0][j] = str(table[0][j]).center(widths[j]).join(margin_norm)
  220. for i in range(1, rows):
  221. for j in range(1, cols):
  222. data = table[i][j]
  223. margin = data.ifbest(margin_best, margin_norm)
  224. table[i][j] = str(data).join(margin).rjust(widths[j] + margins)
  225. # draw chart
  226. chart = ""
  227. if 0 == rows:
  228. return chart
  229. chart += "|"
  230. for cell in table[0]:
  231. chart += "%s|" % (cell)
  232. chart += "\n"
  233. chart += "| " + "-" * (margins + widths[0] - 2) + " |"
  234. for i in range(1, cols):
  235. chart += " " + "-" * (margins + widths[i] - 2) + ":|"
  236. chart += "\n"
  237. for i in range(1, rows):
  238. chart += "|"
  239. for j in range(0, cols):
  240. chart += "%s|" % (table[i][j])
  241. chart += "\n"
  242. return chart
  243. def run_call_site_size(params, result):
  244. if type(result) is not dict:
  245. raise RuntimeError("Not a dictionary")
  246. id = "call_site_size"
  247. params = params[id]
  248. for mode in params:
  249. name = "%s.%s" % (id, mode)
  250. values = dict()
  251. for subj in params[mode]:
  252. data = params[mode][subj]
  253. sz1 = os.path.getsize(data["1"])
  254. sz2 = os.path.getsize(data["2"])
  255. data = data_bytes(sz2 - sz1)
  256. values[subj] = data
  257. result[name] = values
  258. for best in take_best(values.values(),
  259. key=lambda x: x.value,
  260. compare=cmp_percentage(0.0, key=lambda x: x.value)):
  261. best.set_best()
  262. def run_executable_size(params, result):
  263. if type(result) is not dict:
  264. raise RuntimeError("Not a dictionary")
  265. id = "executable_size"
  266. params = params[id]
  267. for mode in params:
  268. name = "%s.%s" % (id, mode)
  269. values = dict()
  270. for subj in params[mode]:
  271. sz = os.path.getsize(params[mode][subj])
  272. values[subj] = data_bytes(sz)
  273. result[name] = values
  274. for best in take_best(values.values(),
  275. key=lambda x: x.value,
  276. compare=cmp_percentage(0.0, key=lambda x: x.value)):
  277. best.set_best()
  278. def run_build_time(params, result, id, optional=False):
  279. if type(result) is not dict:
  280. raise RuntimeError("Not a dictionary")
  281. if optional and id not in params:
  282. return
  283. params = params[id]
  284. values = dict()
  285. for subj in params:
  286. with open(params[subj], "r") as f:
  287. dt = json.load(f)["dt"]
  288. values[subj] = data_seconds(dt)
  289. result[id] = values
  290. for best in take_best(values.values(),
  291. key=lambda x: x.value,
  292. compare=cmp_percentage(0.2, key=lambda x: x.value)):
  293. best.set_best()
  294. def run_speed(params, result, threads_variants, seconds=1):
  295. if type(result) is not dict:
  296. raise RuntimeError("Not a dictionary")
  297. id = "speed"
  298. params = params[id]
  299. for threads in threads_variants:
  300. for mode in params:
  301. name = (id, threads, mode)
  302. values = dict()
  303. for subj in params[mode]:
  304. path = params[mode][subj]
  305. p = subprocess.Popen([path, str(threads), str(seconds)], stdout=subprocess.PIPE)
  306. stdout, stderr = p.communicate()
  307. values[subj] = data_freq(int(stdout), seconds)
  308. result[name] = values
  309. for best in take_best(values.values(),
  310. key=lambda x: x.freq(), reverse=True,
  311. compare=cmp_percentage(0.1, key=lambda x: x.freq())):
  312. best.set_best()
  313. def run_tests(params):
  314. result = dict()
  315. run_call_site_size(params, result)
  316. run_executable_size(params, result)
  317. run_build_time(params, result, "compile_time", optional=True)
  318. run_build_time(params, result, "link_time", optional=True)
  319. run_speed(params, result, take_threads_variants())
  320. return result
  321. def main(argv):
  322. parser = argparse.ArgumentParser()
  323. parser.add_argument("-t", "--text", metavar="PATH", default=None,
  324. help="Text output file path")
  325. parser.add_argument("-m", "--markdown", metavar="PATH", default=None,
  326. help="Markdown output file path")
  327. parser.add_argument("-p", "--parameter", metavar="VALUE", action="append", default=[],
  328. help="Input parameter")
  329. parser.add_argument("-b", "--build", metavar="TYPE",
  330. help="Input parameter")
  331. parser.add_argument("-v", "--verbose", action="store_true",
  332. help="Verbose output")
  333. args = parser.parse_args(argv[1:])
  334. # process parameters
  335. params = dict()
  336. for p in args.parameter:
  337. d = params
  338. vs = p.split(":")
  339. key = None
  340. for i in range(len(vs)):
  341. if i == len(vs) - 1:
  342. d[key] = vs[i]
  343. break
  344. if key is not None:
  345. d = d[key]
  346. key = vs[i]
  347. if key not in d:
  348. d[key] = dict()
  349. if args.verbose:
  350. sys.stderr.write(pprint.pformat(params, indent=4))
  351. sys.stderr.write("\n")
  352. # run, run, run!
  353. result = run_tests(params)
  354. if args.verbose:
  355. sys.stderr.write(pprint.pformat(result, indent=4))
  356. sys.stderr.write("\n")
  357. if args.text is not None:
  358. with open(args.text, "w") as f:
  359. table = gen_table_ascii(result)
  360. f.write(table)
  361. if args.markdown is not None:
  362. with open(args.markdown, "w") as f:
  363. table = gen_table_markdown(result)
  364. f.write(table)
  365. return 0
  366. if __name__ == "__main__":
  367. sys.exit(main(sys.argv))