benchmarker.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. from toolset.utils.output_helper import log, FNULL
  2. from toolset.utils.metadata_helper import gather_tests, gather_remaining_tests
  3. from toolset.utils import docker_helper
  4. import os
  5. import subprocess
  6. import traceback
  7. import socket
  8. import time
  9. import json
  10. import shlex
  11. from pprint import pprint
  12. from colorama import Fore
  13. class Benchmarker:
  14. def __init__(self, config, results):
  15. '''
  16. Initialize the benchmarker.
  17. '''
  18. self.config = config
  19. self.results = results
  20. ##########################################################################################
  21. # Public methods
  22. ##########################################################################################
  23. def run(self):
  24. '''
  25. This process involves setting up the client/server machines
  26. with any necessary change. Then going through each test,
  27. running their docker build and run, verifying the URLs, and
  28. running benchmarks against them.
  29. '''
  30. # Generate metadata
  31. self.__run_list_test_metadata()
  32. # Get a list of all known tests that we can run.
  33. all_tests = gather_remaining_tests(self.config, self.results)
  34. any_failed = False
  35. # Run tests
  36. log("Running Tests...", border='=')
  37. docker_helper.build_wrk(self.config)
  38. with open(os.path.join(self.results.directory, 'benchmark.log'),
  39. 'w') as benchmark_log:
  40. for test in all_tests:
  41. log("Running Test: %s" % test.name, border='-')
  42. with self.config.quiet_out.enable():
  43. if not self.__run_test(test, benchmark_log):
  44. any_failed = True
  45. # Load intermediate result from child process
  46. self.results.load()
  47. # Parse results
  48. if self.config.mode == "benchmark":
  49. log("Parsing Results ...", border='=')
  50. self.results.parse(all_tests)
  51. self.results.set_completion_time()
  52. self.results.upload()
  53. self.results.finish()
  54. return any_failed
  55. ##########################################################################################
  56. # Private methods
  57. ##########################################################################################
  58. def __run_list_test_metadata(self):
  59. '''
  60. Prints the metadata for all the available tests
  61. '''
  62. all_tests = gather_tests(benchmarker_config=self.config)
  63. all_tests_json = json.dumps(map(lambda test: {
  64. "name": test.name,
  65. "approach": test.approach,
  66. "classification": test.classification,
  67. "database": test.database,
  68. "framework": test.framework,
  69. "language": test.language,
  70. "orm": test.orm,
  71. "platform": test.platform,
  72. "webserver": test.webserver,
  73. "os": test.os,
  74. "database_os": test.database_os,
  75. "display_name": test.display_name,
  76. "notes": test.notes,
  77. "versus": test.versus
  78. }, all_tests))
  79. with open(
  80. os.path.join(self.results.directory, "test_metadata.json"),
  81. "w") as f:
  82. f.write(all_tests_json)
  83. def __run_test(self, test, benchmark_log):
  84. '''
  85. Runs the given test, verifies that the webapp is accepting requests,
  86. optionally benchmarks the webapp, and ultimately stops all services
  87. started for this test.
  88. '''
  89. log_prefix = "%s: " % test.name
  90. # If the test is in the excludes list, we skip it
  91. if self.config.exclude != None and test.name in self.config.exclude:
  92. message = "Test {name} has been added to the excludes list. Skipping.".format(name=test.name)
  93. self.results.write_intermediate(test.name, message)
  94. log(message,
  95. prefix=log_prefix,
  96. file=benchmark_log)
  97. return False
  98. database_container = None
  99. try:
  100. if self.__is_port_bound(test.port):
  101. time.sleep(60)
  102. if self.__is_port_bound(test.port):
  103. # We gave it our all
  104. message = "Error: Port %s is not available, cannot start %s" % (test.port, test.name)
  105. self.results.write_intermediate(test.name, message)
  106. log(message,
  107. prefix=log_prefix,
  108. file=benchmark_log,
  109. color=Fore.RED)
  110. return False
  111. # Start database container
  112. if test.database.lower() != "none":
  113. database_container = docker_helper.start_database(
  114. self.config, test.database.lower())
  115. if database_container is None:
  116. message = "ERROR: Problem building/running database container"
  117. self.results.write_intermediate(test.name, message)
  118. log(message,
  119. prefix=log_prefix,
  120. file=benchmark_log,
  121. color=Fore.RED)
  122. return False
  123. # Start webapp
  124. container = test.start()
  125. if container is None:
  126. docker_helper.stop(self.config, container, database_container,
  127. test)
  128. message = "ERROR: Problem starting {name}".format(name=test.name)
  129. self.results.write_intermediate(test.name, message)
  130. log(message,
  131. prefix=log_prefix,
  132. file=benchmark_log,
  133. color=Fore.RED)
  134. return False
  135. slept = 0
  136. max_sleep = 60
  137. accepting_requests = False
  138. while not accepting_requests and slept < max_sleep:
  139. if not docker_helper.server_container_exists(self.config, container.id):
  140. break
  141. accepting_requests = test.is_accepting_requests()
  142. time.sleep(1)
  143. slept += 1
  144. if not accepting_requests:
  145. docker_helper.stop(self.config, container, database_container,
  146. test)
  147. message = "ERROR: Framework is not accepting requests from client machine"
  148. self.results.write_intermediate(test.name, message)
  149. log(message,
  150. prefix=log_prefix,
  151. file=benchmark_log,
  152. color=Fore.RED)
  153. return False
  154. # Debug mode blocks execution here until ctrl+c
  155. if self.config.mode == "debug":
  156. log("Entering debug mode. Server has started. CTRL-c to stop.",
  157. prefix=log_prefix,
  158. file=benchmark_log,
  159. color=Fore.YELLOW)
  160. while True:
  161. time.sleep(1)
  162. # Verify URLs
  163. log("Verifying framework URLs", prefix=log_prefix)
  164. passed_verify = test.verify_urls()
  165. # Benchmark this test
  166. if self.config.mode == "benchmark":
  167. log("Benchmarking %s" % test.name,
  168. file=benchmark_log,
  169. border='-')
  170. self.__benchmark(test, benchmark_log)
  171. # Stop this test
  172. docker_helper.stop(self.config, container, database_container,
  173. test)
  174. # Save results thus far into the latest results directory
  175. self.results.write_intermediate(test.name,
  176. time.strftime(
  177. "%Y%m%d%H%M%S",
  178. time.localtime()))
  179. # Upload the results thus far to another server (optional)
  180. self.results.upload()
  181. if self.config.mode == "verify" and not passed_verify:
  182. log("Failed verify!",
  183. prefix=log_prefix,
  184. file=benchmark_log,
  185. color=Fore.RED)
  186. return False
  187. except Exception as e:
  188. tb = traceback.format_exc()
  189. self.results.write_intermediate(test.name,
  190. "error during test: " + str(e))
  191. log("Error during test: %s" % test.name,
  192. file=benchmark_log,
  193. border='-',
  194. color=Fore.RED)
  195. log(tb, prefix=log_prefix, file=benchmark_log)
  196. return False
  197. return True
  198. def __benchmark(self, framework_test, benchmark_log):
  199. '''
  200. Runs the benchmark for each type of test that it implements
  201. '''
  202. def benchmark_type(test_type):
  203. log("BENCHMARKING %s ... " % test_type.upper(), file=benchmark_log)
  204. test = framework_test.runTests[test_type]
  205. test.setup_out(benchmark_log)
  206. raw_file = self.results.get_raw_file(framework_test.name,
  207. test_type)
  208. if not os.path.exists(raw_file):
  209. # Open to create the empty file
  210. with open(raw_file, 'w'):
  211. pass
  212. if not test.failed:
  213. # Begin resource usage metrics collection
  214. self.__begin_logging(framework_test, test_type)
  215. script = self.config.types[test_type].get_script_name()
  216. script_variables = self.config.types[
  217. test_type].get_script_variables(
  218. test.name, "http://%s:%s%s" % (self.config.server_host,
  219. framework_test.port,
  220. test.get_url()))
  221. docker_helper.benchmark(self.config, script, script_variables,
  222. raw_file)
  223. # End resource usage metrics collection
  224. self.__end_logging()
  225. results = self.results.parse_test(framework_test, test_type)
  226. log("Benchmark results:", file=benchmark_log)
  227. # TODO move into log somehow
  228. pprint(results)
  229. self.results.report_benchmark_results(framework_test, test_type,
  230. results['results'])
  231. log("Complete", file=benchmark_log)
  232. for test_type in framework_test.runTests:
  233. benchmark_type(test_type)
  234. def __begin_logging(self, framework_test, test_type):
  235. '''
  236. Starts a thread to monitor the resource usage, to be synced with the
  237. client's time.
  238. TODO: MySQL and InnoDB are possible. Figure out how to implement them.
  239. '''
  240. output_file = "{file_name}".format(
  241. file_name=self.results.get_stats_file(framework_test.name,
  242. test_type))
  243. dstat_string = "dstat -Tafilmprs --aio --fs --ipc --lock --raw --socket --tcp \
  244. --raw --socket --tcp --udp --unix --vm --disk-util \
  245. --rpc --rpcd --output {output_file}".format(
  246. output_file=output_file)
  247. cmd = shlex.split(dstat_string)
  248. self.subprocess_handle = subprocess.Popen(
  249. cmd, stdout=FNULL, stderr=subprocess.STDOUT)
  250. def __end_logging(self):
  251. '''
  252. Stops the logger thread and blocks until shutdown is complete.
  253. '''
  254. self.subprocess_handle.terminate()
  255. self.subprocess_handle.communicate()
  256. def __is_port_bound(self, port):
  257. '''
  258. Check if the requested port is available. If it isn't available, then a
  259. previous test probably didn't shutdown properly.
  260. '''
  261. port = int(port)
  262. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  263. try:
  264. # Try to bind to all IP addresses, this port
  265. s.bind(("", port))
  266. # If we get here, we were able to bind successfully,
  267. # which means the port is free.
  268. except socket.error:
  269. # If we get an exception, it might be because the port is still bound
  270. # which would be bad, or maybe it is a privileged port (<1024) and we
  271. # are not running as root, or maybe the server is gone, but sockets are
  272. # still in TIME_WAIT (SO_REUSEADDR). To determine which scenario, try to
  273. # connect.
  274. try:
  275. s.connect(("127.0.0.1", port))
  276. # If we get here, we were able to connect to something, which means
  277. # that the port is still bound.
  278. return True
  279. except socket.error:
  280. # An exception means that we couldn't connect, so a server probably
  281. # isn't still running on the port.
  282. pass
  283. finally:
  284. s.close()
  285. return False