utils.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import unittest
  2. import sys
  3. import os
  4. import tempfile
  5. import sys
  6. import time
  7. import json
  8. import urllib
  9. import urllib2
  10. import subprocess
  11. import signal
  12. import random
  13. import operator
  14. from collections import defaultdict
  15. sys.path.append("interface/HUTN")
  16. sys.path.append("scripts")
  17. from hutn_compiler.compiler import main as do_compile
  18. from check_objects import to_recompile
  19. USERNAME = "test_task"
  20. PARALLEL_PUSH = True
  21. BOOTSTRAP_FOLDER_NAME = "bootstrap"
  22. CURRENT_FOLDER_NAME = "performance"
  23. MODELVERSE_TIMEOUT = 600
  24. PORTS = set()
  25. OPTIMIZATION_LEVEL_LEGACY_INTERPRETER = "legacy-interpreter"
  26. OPTIMIZATION_LEVEL_INTERPRETER = "interpreter"
  27. OPTIMIZATION_LEVEL_BYTECODE_INTERPRETER = "bytecode-interpreter"
  28. OPTIMIZATION_LEVEL_BASELINE_JIT = "baseline-jit"
  29. OPTIMIZATION_LEVEL_BASELINE_JIT_NO_THUNKS = "baseline-jit,no-thunks"
  30. OPTIMIZATION_LEVEL_FAST_JIT = "fast-jit"
  31. OPTIMIZATION_LEVEL_FAST_JIT_NO_NOPS = "fast-jit,no-insert-nops"
  32. OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_LARGE_FUNCTIONS = "adaptive-jit-favor-large-functions"
  33. OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_SMALL_FUNCTIONS = "adaptive-jit-favor-small-functions"
  34. OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_LOOPS = "adaptive-jit-favor-loops"
  35. OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_SMALL_LOOPS = "adaptive-jit-favor-small-loops"
  36. ALL_OPTIMIZATION_LEVELS = [
  37. OPTIMIZATION_LEVEL_LEGACY_INTERPRETER,
  38. OPTIMIZATION_LEVEL_INTERPRETER,
  39. OPTIMIZATION_LEVEL_BYTECODE_INTERPRETER,
  40. OPTIMIZATION_LEVEL_BASELINE_JIT,
  41. OPTIMIZATION_LEVEL_BASELINE_JIT_NO_THUNKS,
  42. OPTIMIZATION_LEVEL_FAST_JIT,
  43. OPTIMIZATION_LEVEL_FAST_JIT_NO_NOPS,
  44. OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_LARGE_FUNCTIONS,
  45. OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_SMALL_FUNCTIONS,
  46. OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_LOOPS,
  47. OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_SMALL_LOOPS
  48. ]
  49. class ModelverseTerminated(Exception):
  50. """An exception that tells the task that the Modelverse has terminated."""
  51. pass
  52. def get_code_folder_name():
  53. """Gets the name of the code folder."""
  54. return '%s/code' % CURRENT_FOLDER_NAME
  55. def get_free_port():
  56. """Gets a unique new port."""
  57. while 1:
  58. port = random.randint(10000, 20000)
  59. # Check if this port is in the set of ports.
  60. if port not in PORTS:
  61. # We have found a unique port. Add it to the set and return.
  62. PORTS.add(port)
  63. return port
  64. def execute(scriptname, parameters=None, wait=False):
  65. """Runs a script."""
  66. if os.name not in ["nt", "posix"]:
  67. # Stop now, as we would have no clue on how to kill its subtree
  68. raise Exception("Unknown OS version: " + str(os.name))
  69. command = [sys.executable, "scripts/%s.py" % scriptname] + (
  70. [] if parameters is None else parameters)
  71. if wait:
  72. return subprocess.call(command, shell=False)
  73. else:
  74. return subprocess.Popen(command, shell=False)
  75. def kill(process):
  76. """Kills the given process."""
  77. if os.name == "nt":
  78. subprocess.call(["taskkill", "/F", "/T", "/PID", "%i" % process.pid])
  79. elif os.name == "posix":
  80. subprocess.call(["pkill", "-P", "%i" % process.pid])
  81. def set_input_data(address, data):
  82. """Sets the Modelverse program's input data."""
  83. if data is not None:
  84. urllib2.urlopen(
  85. urllib2.Request(
  86. address,
  87. urllib.urlencode(
  88. {"op": "set_input", "data": json.dumps(data), "taskname": USERNAME})),
  89. timeout=MODELVERSE_TIMEOUT).read()
  90. else:
  91. return []
  92. def compile_file(address, mod_filename, filename, mode, proc):
  93. """Compiles the given file."""
  94. # Load in the file required
  95. try:
  96. timeout_val = MODELVERSE_TIMEOUT
  97. taskname = str(random.random())
  98. while 1:
  99. proc2 = execute(
  100. "compile", [address, mod_filename, taskname, filename, mode], wait=False)
  101. if proc.returncode is not None:
  102. # Modelverse has already terminated, which isn't a good sign!
  103. raise Exception("Modelverse died!")
  104. while proc2.returncode is None:
  105. time.sleep(0.01)
  106. proc2.poll()
  107. timeout_val -= 0.01
  108. if timeout_val < 0:
  109. kill(proc2)
  110. print("Compilation timeout expired!")
  111. return False
  112. if proc2.returncode != 2:
  113. break
  114. # Make sure everything stopped correctly
  115. assert proc2.returncode == 0
  116. if proc2.returncode != 0:
  117. return False
  118. except:
  119. raise
  120. finally:
  121. try:
  122. kill(proc2)
  123. except UnboundLocalError:
  124. pass
  125. def compile_files(address, process, files, mode):
  126. """Compiles the given files in the given mode."""
  127. threads = []
  128. mod_files = []
  129. for filename in files:
  130. if os.path.isfile(filename):
  131. mod_filename = filename
  132. elif os.path.isfile("%s/%s" % (get_code_folder_name(), filename)):
  133. mod_filename = "%s/%s" % (get_code_folder_name(), filename)
  134. elif os.path.isfile("%s/%s" % (BOOTSTRAP_FOLDER_NAME, filename)):
  135. mod_filename = "%s/%s" % (BOOTSTRAP_FOLDER_NAME, filename)
  136. else:
  137. raise Exception("File not found: %s" % filename)
  138. mod_files.append(mod_filename)
  139. to_compile = to_recompile(address, mod_files)
  140. for mod_filename in to_compile:
  141. if mod_filename.endswith(".mvc"):
  142. model_mode = "MO"
  143. mod_files.remove(mod_filename)
  144. else:
  145. model_mode = mode
  146. if PARALLEL_PUSH:
  147. import threading
  148. threads.append(
  149. threading.Thread(
  150. target=compile_file,
  151. args=[address, mod_filename, mod_filename, model_mode, process]))
  152. threads[-1].start()
  153. else:
  154. compile_file(address, mod_filename, mod_filename, model_mode, process)
  155. if PARALLEL_PUSH:
  156. for t in threads:
  157. t.join()
  158. if mode[-1] == "O":
  159. # Fire up the linker
  160. val = execute("link_and_load", [address, USERNAME] + mod_files, wait=True)
  161. if val != 0:
  162. raise Exception("Linking error")
  163. def run_file(files, parameters, mode, handle_output, optimization_level=None, jit_timing_log=None):
  164. """Compiles the given sequence of files, feeds them the given input in the given mode,
  165. and handles their output."""
  166. # Resolve file
  167. import os.path
  168. time.sleep(0.01)
  169. port = get_free_port()
  170. address = "http://127.0.0.1:%i" % port
  171. try:
  172. # Run Modelverse server
  173. modelverse_args = [str(port)]
  174. if optimization_level is not None:
  175. modelverse_args.append('--kernel=%s' % optimization_level)
  176. if jit_timing_log is not None:
  177. modelverse_args.append('--jit-timing-log=%s' % jit_timing_log)
  178. proc = execute("run_local_modelverse", modelverse_args, wait=False)
  179. # Compile, push and link the source code files.
  180. compile_files(address, proc, files, mode)
  181. # Send the request ...
  182. set_input_data(address, parameters)
  183. # ... and wait for replies
  184. while 1:
  185. val = urllib2.urlopen(
  186. urllib2.Request(
  187. address,
  188. urllib.urlencode({"op": "get_output", "taskname": USERNAME})),
  189. timeout=MODELVERSE_TIMEOUT).read()
  190. val = json.loads(val)
  191. if proc.returncode is not None:
  192. # Modelverse has terminated. This may or may not be what we want.
  193. raise ModelverseTerminated()
  194. if not handle_output(val):
  195. return
  196. # All passed!
  197. return
  198. except:
  199. raise
  200. finally:
  201. try:
  202. kill(proc)
  203. except UnboundLocalError:
  204. pass
  205. def run_file_to_completion(files, parameters, mode):
  206. """Compiles the given sequence of files, feeds them the given input in the given mode,
  207. and then collects and returns output."""
  208. results = []
  209. def handle_output(output):
  210. """Appends the given output to the list of results."""
  211. results.append(output)
  212. return True
  213. try:
  214. run_file(files, parameters, mode, handle_output)
  215. except ModelverseTerminated:
  216. return results
  217. def run_file_fixed_output_count(
  218. files, parameters, mode, output_count, optimization_level=None, jit_timing_log=None):
  219. """Compiles the given sequence of files, feeds them the given input in the given mode,
  220. and then collects and returns a fixed number of outputs."""
  221. results = []
  222. def handle_output(output):
  223. """Appends the given output to the list of results."""
  224. results.append(output)
  225. if len(results) < output_count:
  226. return True
  227. else:
  228. return False
  229. run_file(files, parameters, mode, handle_output, optimization_level, jit_timing_log)
  230. return results
  231. def run_file_single_output(files, parameters, mode, optimization_level=None, jit_timing_log=None):
  232. """Compiles the given sequence of files, feeds them the given input in the given mode,
  233. and then collects and returns a single output."""
  234. return run_file_fixed_output_count(
  235. files, parameters, mode, 1, optimization_level, jit_timing_log)[0]
  236. def mean(values):
  237. """Computes the arithmetic mean of the given values."""
  238. return float(sum(values)) / max(len(values), 1)
  239. def parse_jit_timing_log(log_file):
  240. """Parses the JIT timing log entries from the given file."""
  241. results = []
  242. for line in log_file.readlines():
  243. first, _, data = line.strip().rpartition(':')
  244. _, _, name = first.strip().rpartition(' ')
  245. results.append((name, float(data)))
  246. return results
  247. def run_perf_test(files, parameters, optimization_level, n_iterations=1):
  248. """Compiles the given sequence of files, feeds them the given input in the given mode,
  249. and then collects their output. This process is repeated n_iterations times. The
  250. return value is the average of all outputs, along with the mean total run-time."""
  251. test_runtimes = []
  252. total_runtimes = []
  253. compile_times = []
  254. for _ in xrange(n_iterations):
  255. try:
  256. timing_log = tempfile.mktemp()
  257. start_time = time.time()
  258. test_time = run_file_single_output(
  259. files, parameters, 'CO',
  260. optimization_level, timing_log)
  261. end_time = time.time()
  262. total_time = end_time - start_time
  263. test_runtimes.append(test_time)
  264. total_runtimes.append(total_time)
  265. with open(timing_log, 'r') as log_file:
  266. parsed_times = parse_jit_timing_log(log_file)
  267. compile_times.append(sum([data for _, data in parsed_times]))
  268. finally:
  269. os.remove(timing_log)
  270. return {
  271. TEST_TIME_QUANTITY: mean(test_runtimes),
  272. TOTAL_TIME_QUANTITY: mean(total_runtimes),
  273. COMPILE_TIME_QUANTITY: mean(compile_times)
  274. }
  275. def get_expectation_checks(expected_values):
  276. """Converts the given sequence of expected values to a sequence of functions which tell
  277. if an input is allowed. Every function is accompanied by an expected value."""
  278. def get_single_expectation_checks(expectation):
  279. """Gets an expectation checker for a single expected value."""
  280. if isinstance(expectation, set):
  281. # We expect to receive a number of outputs equal to the size of the set, but their
  282. # order does not matter.
  283. for _ in xrange(len(expectation)):
  284. yield lambda val: val in expectation
  285. elif expectation is None:
  286. # Skip output value
  287. yield lambda _: True
  288. else:
  289. yield lambda val: val == expectation
  290. for expectation in expected_values:
  291. for checker in get_single_expectation_checks(expectation):
  292. yield checker, expectation
  293. def run_correctness_test(files, parameters, expected, optimization_level):
  294. """Compiles the given sequence of files, feeds them the given input in the given mode,
  295. and then compares the output with the expected output. The return value is a dictionary
  296. of measured quantities."""
  297. checks = iter(list(get_expectation_checks(expected)))
  298. next_check = [next(checks)]
  299. def handle_output(output):
  300. """Checks the given output against the expected output."""
  301. check, expectation = next_check[0]
  302. print("Got %s, expect %s" % (output, expectation))
  303. assert check(output)
  304. try:
  305. next_check[0] = next(checks)
  306. return True
  307. except StopIteration:
  308. return False
  309. timing_log = tempfile.mktemp()
  310. start_time = time.time()
  311. try:
  312. run_file(files, parameters, 'CO', handle_output, optimization_level, timing_log)
  313. with open(timing_log, 'r') as log_file:
  314. parsed_times = parse_jit_timing_log(log_file)
  315. compile_time = sum([data for _, data in parsed_times])
  316. except ModelverseTerminated:
  317. return
  318. finally:
  319. os.remove(timing_log)
  320. end_time = time.time()
  321. return {
  322. TOTAL_TIME_QUANTITY: end_time - start_time,
  323. COMPILE_TIME_QUANTITY: compile_time
  324. }
  325. def format_output(output):
  326. """Formats the output of `run_file_to_completion` as a string."""
  327. return '\n'.join(output)
  328. def define_perf_test(target_class, test_function, optimization_level):
  329. """Defines a performance test in the given class. The performance test calls the given function
  330. at the given optimization level."""
  331. setattr(
  332. target_class,
  333. 'test_%s' % optimization_level.replace('-', '_').lower(),
  334. lambda self: test_function(self, optimization_level))
  335. def define_perf_tests(target_class, test_function, optimization_levels=None):
  336. """Defines performance tests in the given class. Each test calls the given function."""
  337. if optimization_levels is None:
  338. optimization_levels = ALL_OPTIMIZATION_LEVELS
  339. for opt_level in optimization_levels:
  340. define_perf_test(target_class, test_function, opt_level)
  341. def get_model_constructor(code):
  342. # First change multiple spaces to a tab
  343. code_fragments = code.split("\n")
  344. code_fragments = [i for i in code_fragments if i.strip() != ""]
  345. code_fragments = [i.replace(" ", "\t") for i in code_fragments]
  346. initial_tabs = min([len(i) - len(i.lstrip("\t")) for i in code_fragments])
  347. code_fragments = [i[initial_tabs:] for i in code_fragments]
  348. code = "\n".join(code_fragments)
  349. with open("__model.mvc", "w") as f:
  350. f.write(code)
  351. f.flush()
  352. constructors = do_compile("__model.mvc", "interface/HUTN/grammars/modelling.g", "M") + ["exit"]
  353. return constructors
  354. DEFAULT_PERF_FILE_NAME = 'perf_data.txt'
  355. TOTAL_TIME_QUANTITY = 'total-runtime'
  356. TEST_TIME_QUANTITY = 'test-runtime'
  357. COMPILE_TIME_QUANTITY = 'compile-time'
  358. def write_perf_entry_to_stream(
  359. test_name, optimization_level, quantity,
  360. result, output_stream):
  361. """Writes a performance measurement entry to the given stream."""
  362. output_stream.write('%s:%s:%s:%f\n' % (test_name, optimization_level, quantity, result))
  363. def write_perf_to_file(
  364. test_name, optimization_level, runtimes, file_name=DEFAULT_PERF_FILE_NAME):
  365. """Writes performance data to a file."""
  366. with open(file_name, "a") as perf_file:
  367. for quantity, data_point in runtimes.items():
  368. write_perf_entry_to_stream(
  369. test_name, optimization_level, quantity, data_point, perf_file)
  370. def write_total_runtime_to_file(
  371. test_name, optimization_level, total_runtime, file_name=DEFAULT_PERF_FILE_NAME):
  372. """Writes a total runtime entry to a file."""
  373. with open(file_name, "a") as perf_file:
  374. write_perf_entry_to_stream(
  375. test_name, optimization_level, TOTAL_TIME_QUANTITY, total_runtime, perf_file)
  376. def parse_perf_data(file_name):
  377. """Parses the performance data in the given file."""
  378. results = defaultdict(lambda: defaultdict(list))
  379. with open(file_name, 'r') as perf_file:
  380. for line in perf_file.readlines():
  381. test_name, optimization_level, quantity, result = line.strip().split(':')
  382. results[quantity][optimization_level].append((test_name, float(result)))
  383. return {
  384. quantity: sorted(result_dict.items(), key=operator.itemgetter(0))
  385. for quantity, result_dict in results.items()
  386. }