benchmarker.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  1. from setup.linux import setup_util
  2. from benchmark import framework_test
  3. from benchmark.test_types import *
  4. from utils import header
  5. from utils import gather_tests
  6. from utils import gather_frameworks
  7. from utils import verify_database_connections
  8. import os
  9. import uuid
  10. import shutil
  11. import stat
  12. import json
  13. import requests
  14. import subprocess
  15. import traceback
  16. import time
  17. import pprint
  18. import csv
  19. import sys
  20. import logging
  21. import socket
  22. import threading
  23. import textwrap
  24. import shutil
  25. from pprint import pprint
  26. from contextlib import contextmanager
  27. from multiprocessing import Process
  28. from datetime import datetime
  29. # Cross-platform colored text
  30. from colorama import Fore, Back, Style
  31. # Text-based progress indicators
  32. import progressbar
  33. class Benchmarker:
  34. ##########################################################################################
  35. # Public methods
  36. ##########################################################################################
  37. def clean_all(self):
  38. if os.path.exists(self.results_directory):
  39. for file in os.listdir(self.results_directory):
  40. if not os.path.exists(os.path.dirname(file)):
  41. shutil.rmtree(os.path.join(self.results_directory, file))
  42. else:
  43. os.remove(os.path.join(self.results_directory, file))
  44. subprocess.check_call(["docker", "image", "prune", "-f"])
  45. docker_ids = subprocess.check_output(["docker", "images", "-q"]).splitlines()
  46. for docker_id in docker_ids:
  47. subprocess.check_call(["docker", "image", "rmi", "-f", docker_id])
  48. ############################################################
  49. # Prints all the available tests
  50. ############################################################
  51. def run_list_tests(self):
  52. all_tests = self.__gather_tests
  53. for test in all_tests:
  54. print(test.name)
  55. self.__finish()
  56. ############################################################
  57. # End run_list_tests
  58. ############################################################
  59. ############################################################
  60. # Prints the metadata for all the available tests
  61. ############################################################
  62. def run_list_test_metadata(self):
  63. all_tests = self.__gather_tests
  64. all_tests_json = json.dumps(map(lambda test: {
  65. "name": test.name,
  66. "approach": test.approach,
  67. "classification": test.classification,
  68. "database": test.database,
  69. "framework": test.framework,
  70. "language": test.language,
  71. "orm": test.orm,
  72. "platform": test.platform,
  73. "webserver": test.webserver,
  74. "os": test.os,
  75. "database_os": test.database_os,
  76. "display_name": test.display_name,
  77. "notes": test.notes,
  78. "versus": test.versus
  79. }, all_tests))
  80. with open(os.path.join(self.full_results_directory(), "test_metadata.json"), "w") as f:
  81. f.write(all_tests_json)
  82. ############################################################
  83. # End run_list_test_metadata
  84. ############################################################
  85. ############################################################
  86. # parse_timestamp
  87. # Re-parses the raw data for a given timestamp
  88. ############################################################
  89. def parse_timestamp(self):
  90. all_tests = self.__gather_tests
  91. for test in all_tests:
  92. test.parse_all()
  93. self.__parse_results(all_tests)
  94. self.__finish()
  95. ############################################################
  96. # End parse_timestamp
  97. ############################################################
  98. ############################################################
  99. # Run the tests:
  100. # This process involves setting up the client/server machines
  101. # with any necessary change. Then going through each test,
  102. # running their setup script, verifying the URLs, and
  103. # running benchmarks against them.
  104. ############################################################
  105. def run(self):
  106. ##########################
  107. # Generate metadata
  108. ##########################
  109. self.run_list_test_metadata()
  110. ##########################
  111. # Get a list of all known
  112. # tests that we can run.
  113. ##########################
  114. all_tests = self.__gather_tests
  115. ##########################
  116. # Setup client/server
  117. ##########################
  118. print(header("Preparing Server, Database, and Client ...", top='=', bottom='='))
  119. with self.quiet_out.enable():
  120. self.__setup_server()
  121. self.__setup_database()
  122. self.__setup_client()
  123. ## Check if wrk (and wrk-pipeline) is installed and executable, if not, raise an exception
  124. #if not (os.access("/usr/local/bin/wrk", os.X_OK) and os.access("/usr/local/bin/wrk-pipeline", os.X_OK)):
  125. # raise Exception("wrk and/or wrk-pipeline are not properly installed. Not running tests.")
  126. ##########################
  127. # Run tests
  128. ##########################
  129. print(header("Running Tests...", top='=', bottom='='))
  130. result = self.__run_tests(all_tests)
  131. ##########################
  132. # Parse results
  133. ##########################
  134. if self.mode == "benchmark":
  135. print(header("Parsing Results ...", top='=', bottom='='))
  136. self.__parse_results(all_tests)
  137. self.__set_completion_time()
  138. self.__upload_results()
  139. self.__finish()
  140. return result
  141. ############################################################
  142. # End run
  143. ############################################################
  144. ############################################################
  145. # generate_url(url, port)
  146. # generates a fully qualified URL for accessing a test url
  147. ############################################################
  148. def generate_url(self, url, port):
  149. return self.server_host + ":" + str(port) + url
  150. ############################################################
  151. # End generate_url
  152. ############################################################
  153. ############################################################
  154. # get_output_file(test_name, test_type)
  155. # returns the output file name for this test_name and
  156. # test_type timestamp/test_type/test_name/raw.txt
  157. ############################################################
  158. def get_output_file(self, test_name, test_type):
  159. return os.path.join(self.results_directory, self.timestamp, test_name, test_type, "raw.txt")
  160. ############################################################
  161. # End get_output_file
  162. ############################################################
  163. ############################################################
  164. # output_file(test_name, test_type)
  165. # returns the output file for this test_name and test_type
  166. # timestamp/test_type/test_name/raw.txt
  167. ############################################################
  168. def output_file(self, test_name, test_type):
  169. path = self.get_output_file(test_name, test_type)
  170. try:
  171. os.makedirs(os.path.dirname(path))
  172. except OSError:
  173. pass
  174. return path
  175. ############################################################
  176. # End output_file
  177. ############################################################
  178. ############################################################
  179. # get_stats_file(test_name, test_type)
  180. # returns the stats file name for this test_name and
  181. # test_type timestamp/test_type/test_name/stats.txt
  182. ############################################################
  183. def get_stats_file(self, test_name, test_type):
  184. return os.path.join(self.results_directory, self.timestamp, test_name, test_type, "stats.txt")
  185. ############################################################
  186. # End get_stats_file
  187. ############################################################
  188. ############################################################
  189. # stats_file(test_name, test_type)
  190. # returns the stats file for this test_name and test_type
  191. # timestamp/test_type/test_name/stats.txt
  192. ############################################################
  193. def stats_file(self, test_name, test_type):
  194. path = self.get_stats_file(test_name, test_type)
  195. try:
  196. os.makedirs(os.path.dirname(path))
  197. except OSError:
  198. pass
  199. return path
  200. ############################################################
  201. # End stats_file
  202. ############################################################
  203. ############################################################
  204. # full_results_directory
  205. ############################################################
  206. def full_results_directory(self):
  207. path = os.path.join(self.fwroot, self.results_directory, self.timestamp)
  208. try:
  209. os.makedirs(path)
  210. except OSError:
  211. pass
  212. return path
  213. ############################################################
  214. # End full_results_directory
  215. ############################################################
  216. ############################################################
  217. # report_verify_results
  218. # Used by FrameworkTest to add verification details to our results
  219. #
  220. # TODO: Technically this is an IPC violation - we are accessing
  221. # the parent process' memory from the child process
  222. ############################################################
  223. def report_verify_results(self, framework, test, result):
  224. if framework.name not in self.results['verify'].keys():
  225. self.results['verify'][framework.name] = dict()
  226. self.results['verify'][framework.name][test] = result
  227. ############################################################
  228. # report_benchmark_results
  229. # Used by FrameworkTest to add benchmark data to this
  230. #
  231. # TODO: Technically this is an IPC violation - we are accessing
  232. # the parent process' memory from the child process
  233. ############################################################
  234. def report_benchmark_results(self, framework, test, results):
  235. if test not in self.results['rawData'].keys():
  236. self.results['rawData'][test] = dict()
  237. # If results has a size from the parse, then it succeeded.
  238. if results:
  239. self.results['rawData'][test][framework.name] = results
  240. # This may already be set for single-tests
  241. if framework.name not in self.results['succeeded'][test]:
  242. self.results['succeeded'][test].append(framework.name)
  243. else:
  244. # This may already be set for single-tests
  245. if framework.name not in self.results['failed'][test]:
  246. self.results['failed'][test].append(framework.name)
  247. ############################################################
  248. # End report_results
  249. ############################################################
  250. ##########################################################################################
  251. # Private methods
  252. ##########################################################################################
  253. ############################################################
  254. # Gathers all the tests
  255. ############################################################
  256. @property
  257. def __gather_tests(self):
  258. tests = gather_tests(include=self.test,
  259. exclude=self.exclude,
  260. benchmarker=self)
  261. # If the tests have been interrupted somehow, then we want to resume them where we left
  262. # off, rather than starting from the beginning
  263. if os.path.isfile(self.current_benchmark):
  264. with open(self.current_benchmark, 'r') as interrupted_benchmark:
  265. interrupt_bench = interrupted_benchmark.read().strip()
  266. for index, atest in enumerate(tests):
  267. if atest.name == interrupt_bench:
  268. tests = tests[index:]
  269. break
  270. return tests
  271. ############################################################
  272. # End __gather_tests
  273. ############################################################
  274. ############################################################
  275. # Makes any necessary changes to the server that should be
  276. # made before running the tests. This involves setting kernal
  277. # settings to allow for more connections, or more file
  278. # descriptiors
  279. #
  280. # http://redmine.lighttpd.net/projects/weighttp/wiki#Troubleshooting
  281. ############################################################
  282. def __setup_server(self):
  283. try:
  284. if os.name == 'nt':
  285. return True
  286. subprocess.call(['sudo', 'sysctl', '-w', 'net.ipv4.tcp_max_syn_backlog=65535'], stdout=self.quiet_out, stderr=subprocess.STDOUT)
  287. subprocess.call(['sudo', 'sysctl', '-w', 'net.core.somaxconn=65535'], stdout=self.quiet_out, stderr=subprocess.STDOUT)
  288. subprocess.call(['sudo', 'sysctl', 'net.ipv4.tcp_tw_reuse=1'], stdout=self.quiet_out, stderr=subprocess.STDOUT)
  289. subprocess.call(['sudo', 'sysctl', 'net.ipv4.tcp_tw_recycle=1'], stdout=self.quiet_out, stderr=subprocess.STDOUT)
  290. subprocess.call(['sudo', 'sysctl', '-w', 'kernel.shmmax=134217728'], stdout=self.quiet_out, stderr=subprocess.STDOUT)
  291. subprocess.call(['sudo', 'sysctl', '-w', 'kernel.shmall=2097152'], stdout=self.quiet_out, stderr=subprocess.STDOUT)
  292. with open(os.path.join(self.full_results_directory(), 'sysctl.txt'), 'w') as f:
  293. f.write(subprocess.check_output(['sudo','sysctl','-a']))
  294. except subprocess.CalledProcessError:
  295. return False
  296. ############################################################
  297. # End __setup_server
  298. ############################################################
  299. ############################################################
  300. # Clean up any processes that run with root privileges
  301. ############################################################
  302. def __cleanup_leftover_processes_before_test(self):
  303. p = subprocess.Popen(self.database_ssh_string, stdin=subprocess.PIPE, shell=True, stdout=self.quiet_out, stderr=subprocess.STDOUT)
  304. p.communicate("""
  305. sudo service mysql stop
  306. sudo service mongod stop
  307. sudo kill -9 $(pgrep postgres)
  308. sudo kill -9 $(pgrep mysql)
  309. sudo kill -9 $(pgrep mongo)
  310. """)
  311. ############################################################
  312. # Makes any necessary changes to the database machine that
  313. # should be made before running the tests. Is very similar
  314. # to the server setup, but may also include database specific
  315. # changes.
  316. ############################################################
  317. def __setup_database(self):
  318. p = subprocess.Popen(self.database_ssh_string, stdin=subprocess.PIPE, shell=True, stdout=self.quiet_out, stderr=subprocess.STDOUT)
  319. p.communicate("""
  320. sudo sysctl -w net.ipv4.tcp_max_syn_backlog=65535
  321. sudo sysctl -w net.core.somaxconn=65535
  322. sudo sysctl -w kernel.sched_autogroup_enabled=0
  323. sudo -s ulimit -n 65535
  324. sudo sysctl net.ipv4.tcp_tw_reuse=1
  325. sudo sysctl net.ipv4.tcp_tw_recycle=1
  326. sudo sysctl -w kernel.shmmax=2147483648
  327. sudo sysctl -w kernel.shmall=2097152
  328. sudo sysctl -w kernel.sem="250 32000 256 512"
  329. """)
  330. # TODO - print kernel configuration to file
  331. # echo "Printing kernel configuration:" && sudo sysctl -a
  332. # Explanations:
  333. # net.ipv4.tcp_max_syn_backlog, net.core.somaxconn, kernel.sched_autogroup_enabled: http://tweaked.io/guide/kernel/
  334. # ulimit -n: http://www.cyberciti.biz/faq/linux-increase-the-maximum-number-of-open-files/
  335. # net.ipv4.tcp_tw_*: http://www.linuxbrigade.com/reduce-time_wait-socket-connections/
  336. # kernel.shm*: http://seriousbirder.com/blogs/linux-understanding-shmmax-and-shmall-settings/
  337. # For kernel.sem: https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/5/html/Tuning_and_Optimizing_Red_Hat_Enterprise_Linux_for_Oracle_9i_and_10g_Databases/chap-Oracle_9i_and_10g_Tuning_Guide-Setting_Semaphores.html
  338. ############################################################
  339. # End __setup_database
  340. ############################################################
  341. ############################################################
  342. # Sets up a container for the given database and port, and
  343. # starts said docker container.
  344. ############################################################
  345. def __setup_database_container(self, database, port):
  346. def __is_hex(s):
  347. try:
  348. int(s, 16)
  349. except ValueError:
  350. return False
  351. return len(s) % 2 == 0
  352. p = subprocess.Popen(self.database_ssh_string, stdin=subprocess.PIPE, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  353. (out,err) = p.communicate("docker images -q %s" % database)
  354. dbid = ''
  355. if len(out.splitlines()) > 0:
  356. dbid = out.splitlines()[len(out.splitlines()) - 1]
  357. # If the database image exists, then dbid will look like
  358. # fe12ca519b47, and we do not want to rebuild if it exists
  359. if len(dbid) != 12 and not __is_hex(dbid):
  360. def __scp_string(files):
  361. scpstr = ["scp", "-i", self.database_identity_file]
  362. for file in files:
  363. scpstr.append(file)
  364. scpstr.append("%s@%s:~/%s/" % (self.database_user, self.database_host, database))
  365. return scpstr
  366. p = subprocess.Popen(self.database_ssh_string, shell=True, stdin=subprocess.PIPE, stdout=self.quiet_out, stderr=subprocess.STDOUT)
  367. p.communicate("mkdir -p %s" % database)
  368. dbpath = os.path.join(self.fwroot, "toolset", "setup", "linux", "docker", "databases", database)
  369. dbfiles = ""
  370. for dbfile in os.listdir(dbpath):
  371. dbfiles += "%s " % os.path.join(dbpath,dbfile)
  372. p = subprocess.Popen(__scp_string(dbfiles.split()), stdin=subprocess.PIPE, stdout=self.quiet_out, stderr=subprocess.STDOUT)
  373. p.communicate()
  374. p = subprocess.Popen(self.database_ssh_string, shell=True, stdin=subprocess.PIPE, stdout=self.quiet_out, stderr=subprocess.STDOUT)
  375. p.communicate("docker build -f ~/%s/%s.dockerfile -t %s ~/%s" % (database, database, database, database))
  376. if p.returncode != 0:
  377. return None
  378. p = subprocess.Popen(self.database_ssh_string, stdin=subprocess.PIPE, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  379. (out,err) = p.communicate("docker run -d --rm -p %s:%s --network=host %s" % (port,port,database))
  380. return out.splitlines()[len(out.splitlines()) - 1]
  381. ############################################################
  382. # End __setup_database_container
  383. ############################################################
  384. ############################################################
  385. # Makes any necessary changes to the client machine that
  386. # should be made before running the tests. Is very similar
  387. # to the server setup, but may also include client specific
  388. # changes.
  389. ############################################################
  390. def __setup_client(self):
  391. p = subprocess.Popen(self.client_ssh_string, stdin=subprocess.PIPE, shell=True, stdout=self.quiet_out, stderr=subprocess.STDOUT)
  392. p.communicate("""
  393. sudo sysctl -w net.ipv4.tcp_max_syn_backlog=65535
  394. sudo sysctl -w net.core.somaxconn=65535
  395. sudo -s ulimit -n 65535
  396. sudo sysctl net.ipv4.tcp_tw_reuse=1
  397. sudo sysctl net.ipv4.tcp_tw_recycle=1
  398. sudo sysctl -w kernel.shmmax=2147483648
  399. sudo sysctl -w kernel.shmall=2097152
  400. """)
  401. ############################################################
  402. # End __setup_client
  403. ############################################################
  404. ############################################################
  405. # __run_tests
  406. #
  407. # 2013-10-02 ASB Calls each test passed in tests to
  408. # __run_test in a separate process. Each
  409. # test is given a set amount of time and if
  410. # kills the child process (and subsequently
  411. # all of its child processes). Uses
  412. # multiprocessing module.
  413. ############################################################
  414. def __run_tests(self, tests):
  415. if len(tests) == 0:
  416. return 0
  417. logging.debug("Start __run_tests.")
  418. logging.debug("__name__ = %s",__name__)
  419. error_happened = False
  420. if self.os.lower() == 'windows':
  421. logging.debug("Executing __run_tests on Windows")
  422. for test in tests:
  423. with open(self.current_benchmark, 'w') as benchmark_resume_file:
  424. benchmark_resume_file.write(test.name)
  425. with self.quiet_out.enable():
  426. if self.__run_test(test) != 0:
  427. error_happened = True
  428. else:
  429. logging.debug("Executing __run_tests on Linux")
  430. # Setup a nice progressbar and ETA indicator
  431. widgets = [self.mode, ': ', progressbar.Percentage(),
  432. ' ', progressbar.Bar(),
  433. ' Rough ', progressbar.ETA()]
  434. pbar = progressbar.ProgressBar(widgets=widgets, maxval=len(tests)).start()
  435. pbar_test = 0
  436. # These features do not work on Windows
  437. for test in tests:
  438. pbar.update(pbar_test)
  439. pbar_test = pbar_test + 1
  440. if __name__ == 'benchmark.benchmarker':
  441. print(header("Running Test: %s" % test.name))
  442. with open(self.current_benchmark, 'w') as benchmark_resume_file:
  443. benchmark_resume_file.write(test.name)
  444. with self.quiet_out.enable():
  445. test_process = Process(target=self.__run_test, name="Test Runner (%s)" % test.name, args=(test,))
  446. test_process.start()
  447. test_process.join(self.run_test_timeout_seconds)
  448. self.__load_results() # Load intermediate result from child process
  449. if(test_process.is_alive()):
  450. logging.debug("Child process for {name} is still alive. Terminating.".format(name=test.name))
  451. self.__write_intermediate_results(test.name,"__run_test timeout (="+ str(self.run_test_timeout_seconds) + " seconds)")
  452. test_process.terminate()
  453. test_process.join()
  454. if test_process.exitcode != 0:
  455. error_happened = True
  456. pbar.finish()
  457. if os.path.isfile(self.current_benchmark):
  458. os.remove(self.current_benchmark)
  459. logging.debug("End __run_tests.")
  460. if error_happened:
  461. return 1
  462. return 0
  463. ############################################################
  464. # End __run_tests
  465. ############################################################
  466. ############################################################
  467. # __run_test
  468. # 2013-10-02 ASB Previously __run_tests. This code now only
  469. # processes a single test.
  470. #
  471. # Ensures that the system has all necessary software to run
  472. # the tests. This does not include that software for the individual
  473. # test, but covers software such as curl and weighttp that
  474. # are needed.
  475. ############################################################
  476. def __run_test(self, test):
  477. logDir = os.path.join(self.full_results_directory(), test.name.lower())
  478. try:
  479. os.makedirs(logDir)
  480. except Exception:
  481. pass
  482. with open(os.path.join(logDir, 'out.txt'), 'w') as out:
  483. if test.os.lower() != self.os.lower() or test.database_os.lower() != self.database_os.lower():
  484. out.write("OS or Database OS specified in benchmark_config.json does not match the current environment. Skipping.\n")
  485. return sys.exit(0)
  486. # If the test is in the excludes list, we skip it
  487. if self.exclude != None and test.name in self.exclude:
  488. out.write("Test {name} has been added to the excludes list. Skipping.\n".format(name=test.name))
  489. return sys.exit(0)
  490. out.write("test.os.lower() = {os} test.database_os.lower() = {dbos}\n".format(os=test.os.lower(),dbos=test.database_os.lower()))
  491. out.write("self.results['frameworks'] != None: {val}\n".format(val=str(self.results['frameworks'] != None)))
  492. out.write("test.name: {name}\n".format(name=str(test.name)))
  493. out.write("self.results['completed']: {completed}\n".format(completed=str(self.results['completed'])))
  494. if self.results['frameworks'] != None and test.name in self.results['completed']:
  495. out.write('Framework {name} found in latest saved data. Skipping.\n'.format(name=str(test.name)))
  496. print('WARNING: Test {test} exists in the results directory; this must be removed before running a new test.\n'.format(test=str(test.name)))
  497. return sys.exit(1)
  498. out.flush()
  499. out.write(header("Beginning %s" % test.name, top='='))
  500. out.flush()
  501. ##########################
  502. # Start this test
  503. ##########################
  504. out.write(header("Starting %s" % test.name))
  505. out.flush()
  506. database_container_id = None
  507. try:
  508. # self.__cleanup_leftover_processes_before_test()
  509. if self.__is_port_bound(test.port):
  510. time.sleep(60)
  511. if self.__is_port_bound(test.port):
  512. # We gave it our all
  513. self.__write_intermediate_results(test.name, "port " + str(test.port) + " is not available before start")
  514. out.write(header("Error: Port %s is not available, cannot start %s" % (test.port, test.name)))
  515. out.flush()
  516. print("Error: Unable to recover port, cannot start test")
  517. return sys.exit(1)
  518. ##########################
  519. # Start database container
  520. ##########################
  521. if test.database != "None":
  522. # TODO: this is horrible... how should we really do it?
  523. ports = {
  524. "mysql": 3306,
  525. "postgres": 5432,
  526. "mongodb": 27017
  527. }
  528. database_container_id = self.__setup_database_container(test.database.lower(), ports[test.database.lower()])
  529. if not database_container_id:
  530. out.write("ERROR: Problem building/running database container")
  531. out.flush()
  532. self.__write_intermediate_results(test.name,"ERROR: Problem starting")
  533. return sys.exit(1)
  534. ##########################
  535. # Start webapp
  536. ##########################
  537. result = test.start(out)
  538. if result != 0:
  539. self.__stop_test(database_container_id, test, out)
  540. time.sleep(5)
  541. out.write( "ERROR: Problem starting {name}\n".format(name=test.name) )
  542. out.flush()
  543. self.__write_intermediate_results(test.name,"ERROR: Problem starting")
  544. return sys.exit(1)
  545. logging.info("Sleeping %s seconds to ensure framework is ready" % self.sleep)
  546. time.sleep(self.sleep)
  547. ##########################
  548. # Verify URLs
  549. ##########################
  550. if self.mode == "debug":
  551. logging.info("Entering debug mode. Server has started. CTRL-c to stop.")
  552. while True:
  553. time.sleep(1)
  554. else:
  555. logging.info("Verifying framework URLs")
  556. passed_verify = test.verify_urls(logDir)
  557. ##########################
  558. # Benchmark this test
  559. ##########################
  560. if self.mode == "benchmark":
  561. logging.info("Benchmarking")
  562. out.write(header("Benchmarking %s" % test.name))
  563. out.flush()
  564. test.benchmark(logDir)
  565. ##########################
  566. # Stop this test
  567. ##########################
  568. self.__stop_test(database_container_id, test, out)
  569. self.__stop_database(database_container_id, out)
  570. out.write(header("Stopped %s" % test.name))
  571. out.flush()
  572. ##########################################################
  573. # Remove contents of /tmp folder
  574. ##########################################################
  575. try:
  576. subprocess.check_call('sudo rm -rf /tmp/*', shell=True, stderr=out, stdout=out)
  577. except Exception:
  578. out.write(header("Error: Could not empty /tmp"))
  579. ##########################################################
  580. # Remove apt sources to avoid pkg errors and collisions
  581. ##########################################################
  582. os.system("sudo rm -rf /etc/apt/sources.list.d/*")
  583. ##########################################################
  584. # Save results thus far into the latest results directory
  585. ##########################################################
  586. out.write(header("Saving results through %s" % test.name))
  587. out.flush()
  588. self.__write_intermediate_results(test.name,time.strftime("%Y%m%d%H%M%S", time.localtime()))
  589. ##########################################################
  590. # Upload the results thus far to another server (optional)
  591. ##########################################################
  592. self.__upload_results()
  593. if self.mode == "verify" and not passed_verify:
  594. print("Failed verify!")
  595. return sys.exit(1)
  596. except KeyboardInterrupt:
  597. self.__stop_test(database_container_id, test, out)
  598. self.__stop_database(database_container_id, out)
  599. except (OSError, IOError, subprocess.CalledProcessError) as e:
  600. self.__write_intermediate_results(test.name,"<setup.py> raised an exception")
  601. out.write(header("Subprocess Error %s" % test.name))
  602. traceback.print_exc(file=out)
  603. out.flush()
  604. out.close()
  605. return sys.exit(1)
  606. out.close()
  607. return sys.exit(0)
  608. ############################################################
  609. # End __run_tests
  610. ############################################################
  611. ############################################################
  612. # __stop_database
  613. # Attempts to stop the running database container.
  614. ############################################################
  615. def __stop_database(self, database_container_id, out):
  616. if database_container_id:
  617. p = subprocess.Popen(self.database_ssh_string, stdin=subprocess.PIPE, shell=True, stdout=self.quiet_out, stderr=subprocess.STDOUT)
  618. p.communicate("docker stop %s" % database_container_id)
  619. ############################################################
  620. # __stop_test
  621. # Attempts to stop the running test container.
  622. ############################################################
  623. def __stop_test(self, database_container_id, test, out):
  624. docker_ids = subprocess.check_output(["docker", "ps", "-q"]).splitlines()
  625. for docker_id in docker_ids:
  626. # This check is in case the database and server machines are the same
  627. if docker_id:
  628. if not database_container_id or docker_id not in database_container_id:
  629. subprocess.check_output(["docker", "kill", docker_id])
  630. slept = 0
  631. while(slept < 300 and docker_id is ''):
  632. time.sleep(1)
  633. slept += 1
  634. docker_id = subprocess.check_output(["docker", "ps", "-q"]).strip()
  635. # We still need to sleep a bit before removing the image
  636. time.sleep(5)
  637. subprocess.check_call(["docker", "image", "rm", "tfb/test/%s" % test.name])
  638. time.sleep(5)
  639. subprocess.check_call(["docker", "image", "prune", "-f"])
  640. ############################################################
  641. # End __stop_test
  642. ############################################################
  643. def is_port_bound(self, port):
  644. return self.__is_port_bound(port)
  645. ############################################################
  646. # __is_port_bound
  647. # Check if the requested port is available. If it
  648. # isn't available, then a previous test probably didn't
  649. # shutdown properly.
  650. ############################################################
  651. def __is_port_bound(self, port):
  652. port = int(port)
  653. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  654. try:
  655. # Try to bind to all IP addresses, this port
  656. s.bind(("", port))
  657. # If we get here, we were able to bind successfully,
  658. # which means the port is free.
  659. except socket.error:
  660. # If we get an exception, it might be because the port is still bound
  661. # which would be bad, or maybe it is a privileged port (<1024) and we
  662. # are not running as root, or maybe the server is gone, but sockets are
  663. # still in TIME_WAIT (SO_REUSEADDR). To determine which scenario, try to
  664. # connect.
  665. try:
  666. s.connect(("127.0.0.1", port))
  667. # If we get here, we were able to connect to something, which means
  668. # that the port is still bound.
  669. return True
  670. except socket.error:
  671. # An exception means that we couldn't connect, so a server probably
  672. # isn't still running on the port.
  673. pass
  674. finally:
  675. s.close()
  676. return False
  677. ############################################################
  678. # End __is_port_bound
  679. ############################################################
  680. ############################################################
  681. # __parse_results
  682. # Ensures that the system has all necessary software to run
  683. # the tests. This does not include that software for the individual
  684. # test, but covers software such as curl and weighttp that
  685. # are needed.
  686. ############################################################
  687. def __parse_results(self, tests):
  688. # Run the method to get the commmit count of each framework.
  689. self.__count_commits()
  690. # Call the method which counts the sloc for each framework
  691. self.__count_sloc()
  692. # Time to create parsed files
  693. # Aggregate JSON file
  694. with open(os.path.join(self.full_results_directory(), "results.json"), "w") as f:
  695. f.write(json.dumps(self.results, indent=2))
  696. ############################################################
  697. # End __parse_results
  698. ############################################################
  699. #############################################################
  700. # __count_sloc
  701. #############################################################
  702. def __count_sloc(self):
  703. frameworks = gather_frameworks(include=self.test,
  704. exclude=self.exclude, benchmarker=self)
  705. jsonResult = {}
  706. for framework, testlist in frameworks.iteritems():
  707. if not os.path.exists(os.path.join(testlist[0].directory, "source_code")):
  708. logging.warn("Cannot count lines of code for %s - no 'source_code' file", framework)
  709. continue
  710. # Unfortunately the source_code files use lines like
  711. # ./cpoll_cppsp/www/fortune_old instead of
  712. # ./www/fortune_old
  713. # so we have to back our working dir up one level
  714. wd = os.path.dirname(testlist[0].directory)
  715. try:
  716. command = "cloc --list-file=%s/source_code --yaml" % testlist[0].directory
  717. if os.path.exists(os.path.join(testlist[0].directory, "cloc_defs.txt")):
  718. command += " --read-lang-def %s" % os.path.join(testlist[0].directory, "cloc_defs.txt")
  719. logging.info("Using custom cloc definitions for %s", framework)
  720. # Find the last instance of the word 'code' in the yaml output. This should
  721. # be the line count for the sum of all listed files or just the line count
  722. # for the last file in the case where there's only one file listed.
  723. command = command + "| grep code | tail -1 | cut -d: -f 2"
  724. logging.debug("Running \"%s\" (cwd=%s)", command, wd)
  725. lineCount = subprocess.check_output(command, cwd=wd, shell=True)
  726. jsonResult[framework] = int(lineCount)
  727. except subprocess.CalledProcessError:
  728. continue
  729. except ValueError as ve:
  730. logging.warn("Unable to get linecount for %s due to error '%s'", framework, ve)
  731. self.results['rawData']['slocCounts'] = jsonResult
  732. ############################################################
  733. # End __count_sloc
  734. ############################################################
  735. ############################################################
  736. # __count_commits
  737. #
  738. ############################################################
  739. def __count_commits(self):
  740. frameworks = gather_frameworks(include=self.test,
  741. exclude=self.exclude, benchmarker=self)
  742. def count_commit(directory, jsonResult):
  743. command = "git rev-list HEAD -- " + directory + " | sort -u | wc -l"
  744. try:
  745. commitCount = subprocess.check_output(command, shell=True)
  746. jsonResult[framework] = int(commitCount)
  747. except subprocess.CalledProcessError:
  748. pass
  749. # Because git can be slow when run in large batches, this
  750. # calls git up to 4 times in parallel. Normal improvement is ~3-4x
  751. # in my trials, or ~100 seconds down to ~25
  752. # This is safe to parallelize as long as each thread only
  753. # accesses one key in the dictionary
  754. threads = []
  755. jsonResult = {}
  756. t1 = datetime.now()
  757. for framework, testlist in frameworks.iteritems():
  758. directory = testlist[0].directory
  759. t = threading.Thread(target=count_commit, args=(directory,jsonResult))
  760. t.start()
  761. threads.append(t)
  762. # Git has internal locks, full parallel will just cause contention
  763. # and slowness, so we rate-limit a bit
  764. if len(threads) >= 4:
  765. threads[0].join()
  766. threads.remove(threads[0])
  767. # Wait for remaining threads
  768. for t in threads:
  769. t.join()
  770. t2 = datetime.now()
  771. # print "Took %s seconds " % (t2 - t1).seconds
  772. self.results['rawData']['commitCounts'] = jsonResult
  773. self.commits = jsonResult
  774. ############################################################
  775. # End __count_commits
  776. ############################################################
  777. def __write_intermediate_results(self,test_name,status_message):
  778. self.results["completed"][test_name] = status_message
  779. self.__write_results()
  780. def __write_results(self):
  781. try:
  782. with open(os.path.join(self.full_results_directory(), 'results.json'), 'w') as f:
  783. f.write(json.dumps(self.results, indent=2))
  784. except (IOError):
  785. logging.error("Error writing results.json")
  786. def __set_completion_time(self):
  787. self.results['completionTime'] = int(round(time.time() * 1000))
  788. self.__write_results()
  789. def __upload_results(self):
  790. if self.results_upload_uri != None:
  791. try:
  792. requests.post(self.results_upload_uri, headers={ 'Content-Type': 'application/json' }, data=json.dumps(self.results, indent=2))
  793. except (Exception):
  794. logging.error("Error uploading results.json")
  795. def __load_results(self):
  796. try:
  797. with open(os.path.join(self.full_results_directory(), 'results.json')) as f:
  798. self.results = json.load(f)
  799. except (ValueError, IOError):
  800. pass
  801. def __get_git_commit_id(self):
  802. return subprocess.check_output(["git", "rev-parse", "HEAD"]).strip()
  803. def __get_git_repository_url(self):
  804. return subprocess.check_output(["git", "config", "--get", "remote.origin.url"]).strip()
  805. ############################################################
  806. # __finish
  807. ############################################################
  808. def __finish(self):
  809. if not self.list_tests and not self.parse:
  810. tests = self.__gather_tests
  811. # Normally you don't have to use Fore.BLUE before each line, but
  812. # Travis-CI seems to reset color codes on newline (see travis-ci/travis-ci#2692)
  813. # or stream flush, so we have to ensure that the color code is printed repeatedly
  814. prefix = Fore.CYAN
  815. for line in header("Verification Summary", top='=', bottom='').split('\n'):
  816. print(prefix + line)
  817. for test in tests:
  818. print(prefix + "| Test: {!s}".format(test.name))
  819. if test.name in self.results['verify'].keys():
  820. for test_type, result in self.results['verify'][test.name].iteritems():
  821. if result.upper() == "PASS":
  822. color = Fore.GREEN
  823. elif result.upper() == "WARN":
  824. color = Fore.YELLOW
  825. else:
  826. color = Fore.RED
  827. print(prefix + "| " + test_type.ljust(13) + ' : ' + color + result.upper())
  828. else:
  829. print(prefix + "| " + Fore.RED + "NO RESULTS (Did framework launch?)")
  830. print(prefix + header('', top='', bottom='=') + Style.RESET_ALL)
  831. print("Time to complete: " + str(int(time.time() - self.start_time)) + " seconds")
  832. print("Results are saved in " + os.path.join(self.results_directory, self.timestamp))
  833. ############################################################
  834. # End __finish
  835. ############################################################
  836. ##########################################################################################
  837. # Constructor
  838. ##########################################################################################
  839. ############################################################
  840. # Initialize the benchmarker. The args are the arguments
  841. # parsed via argparser.
  842. ############################################################
  843. def __init__(self, args):
  844. # Map type strings to their objects
  845. types = dict()
  846. types['json'] = JsonTestType()
  847. types['db'] = DBTestType()
  848. types['query'] = QueryTestType()
  849. types['fortune'] = FortuneTestType()
  850. types['update'] = UpdateTestType()
  851. types['plaintext'] = PlaintextTestType()
  852. types['cached_query'] = CachedQueryTestType()
  853. # Turn type into a map instead of a string
  854. if args['type'] == 'all':
  855. args['types'] = types
  856. else:
  857. args['types'] = { args['type'] : types[args['type']] }
  858. del args['type']
  859. args['max_concurrency'] = max(args['concurrency_levels'])
  860. if 'pipeline_concurrency_levels' not in args:
  861. args['pipeline_concurrency_levels'] = [256,1024,4096,16384]
  862. self.__dict__.update(args)
  863. self.quiet_out = QuietOutputStream(self.quiet)
  864. self.start_time = time.time()
  865. self.run_test_timeout_seconds = 7200
  866. # setup logging
  867. logging.basicConfig(stream=self.quiet_out, level=logging.INFO)
  868. # setup some additional variables
  869. if self.database_user == None: self.database_user = self.client_user
  870. if self.database_host == None: self.database_host = self.client_host
  871. if self.database_identity_file == None: self.database_identity_file = self.client_identity_file
  872. # Remember root directory
  873. self.fwroot = setup_util.get_fwroot()
  874. # setup current_benchmark.txt location
  875. self.current_benchmark = "/tmp/current_benchmark.txt"
  876. if hasattr(self, 'parse') and self.parse != None:
  877. self.timestamp = self.parse
  878. else:
  879. self.timestamp = time.strftime("%Y%m%d%H%M%S", time.localtime())
  880. # setup results and latest_results directories
  881. self.results_directory = os.path.join(self.fwroot, "results")
  882. self.results = None
  883. try:
  884. with open(os.path.join(self.full_results_directory(), 'results.json'), 'r') as f:
  885. #Load json file into results object
  886. self.results = json.load(f)
  887. except IOError:
  888. logging.warn("results.json for test not found.")
  889. if self.results == None:
  890. self.results = dict()
  891. self.results['uuid'] = str(uuid.uuid4())
  892. self.results['name'] = datetime.now().strftime(self.results_name)
  893. self.results['environmentDescription'] = self.results_environment
  894. try:
  895. self.results['git'] = dict()
  896. self.results['git']['commitId'] = self.__get_git_commit_id()
  897. self.results['git']['repositoryUrl'] = self.__get_git_repository_url()
  898. except Exception as e:
  899. logging.debug('Could not read local git repository, which is fine. The error was: %s', e)
  900. self.results['git'] = None
  901. self.results['startTime'] = int(round(time.time() * 1000))
  902. self.results['completionTime'] = None
  903. self.results['concurrencyLevels'] = self.concurrency_levels
  904. self.results['pipelineConcurrencyLevels'] = self.pipeline_concurrency_levels
  905. self.results['queryIntervals'] = self.query_levels
  906. self.results['cachedQueryIntervals'] = self.cached_query_levels
  907. self.results['frameworks'] = [t.name for t in self.__gather_tests]
  908. self.results['duration'] = self.duration
  909. self.results['rawData'] = dict()
  910. self.results['rawData']['json'] = dict()
  911. self.results['rawData']['db'] = dict()
  912. self.results['rawData']['query'] = dict()
  913. self.results['rawData']['fortune'] = dict()
  914. self.results['rawData']['update'] = dict()
  915. self.results['rawData']['plaintext'] = dict()
  916. self.results['rawData']['cached_query'] = dict()
  917. self.results['completed'] = dict()
  918. self.results['succeeded'] = dict()
  919. self.results['succeeded']['json'] = []
  920. self.results['succeeded']['db'] = []
  921. self.results['succeeded']['query'] = []
  922. self.results['succeeded']['fortune'] = []
  923. self.results['succeeded']['update'] = []
  924. self.results['succeeded']['plaintext'] = []
  925. self.results['succeeded']['cached_query'] = []
  926. self.results['failed'] = dict()
  927. self.results['failed']['json'] = []
  928. self.results['failed']['db'] = []
  929. self.results['failed']['query'] = []
  930. self.results['failed']['fortune'] = []
  931. self.results['failed']['update'] = []
  932. self.results['failed']['plaintext'] = []
  933. self.results['failed']['cached_query'] = []
  934. self.results['verify'] = dict()
  935. else:
  936. #for x in self.__gather_tests():
  937. # if x.name not in self.results['frameworks']:
  938. # self.results['frameworks'] = self.results['frameworks'] + [x.name]
  939. # Always overwrite framework list
  940. self.results['frameworks'] = [t.name for t in self.__gather_tests]
  941. # Setup the ssh command string
  942. self.database_ssh_string = "ssh -T -o StrictHostKeyChecking=no " + self.database_user + "@" + self.database_host
  943. self.client_ssh_string = "ssh -T -o StrictHostKeyChecking=no " + self.client_user + "@" + self.client_host
  944. if self.database_identity_file != None:
  945. self.database_ssh_string = self.database_ssh_string + " -i " + self.database_identity_file
  946. if self.client_identity_file != None:
  947. self.client_ssh_string = self.client_ssh_string + " -i " + self.client_identity_file
  948. self.__process = None
  949. ############################################################
  950. # End __init__
  951. ############################################################
  952. class QuietOutputStream:
  953. def __init__(self, is_quiet):
  954. self.is_quiet = is_quiet
  955. self.null_out = open(os.devnull, 'w')
  956. def fileno(self):
  957. with self.enable():
  958. return sys.stdout.fileno()
  959. def write(self, message):
  960. with self.enable():
  961. sys.stdout.write(message)
  962. @contextmanager
  963. def enable(self):
  964. if self.is_quiet:
  965. old_out = sys.stdout
  966. old_err = sys.stderr
  967. try:
  968. sys.stdout = self.null_out
  969. sys.stderr = self.null_out
  970. yield
  971. finally:
  972. sys.stdout = old_out
  973. sys.stderr = old_err
  974. else:
  975. yield