framework_test.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. import importlib
  2. import os
  3. import subprocess
  4. import time
  5. import re
  6. import pprint
  7. import sys
  8. class FrameworkTest:
  9. ##########################################################################################
  10. # Class variables
  11. ##########################################################################################
  12. headers_template = "-H 'Host: localhost' -H '{accept}' -H 'Connection: keep-alive'"
  13. headers_full_template = "-H 'Host: localhost' -H '{accept}' -H 'Accept-Language: en-US,en;q=0.5' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) Gecko/20130501 Firefox/30.0 AppleWebKit/600.00 Chrome/30.0.0000.0 Trident/10.0 Safari/600.00' -H 'Cookie: uid=12345678901234567890; __utma=1.1234567890.1234567890.1234567890.1234567890.12; wd=2560x1600' -H 'Connection: keep-alive'"
  14. accept_json = "Accept: application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7"
  15. accept_html = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
  16. accept_plaintext = "Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7"
  17. concurrency_template = """
  18. echo ""
  19. echo "---------------------------------------------------------"
  20. echo " Running Primer {name}"
  21. echo " {wrk} {headers} -d 60 -c 8 -t 8 \"http://{server_host}:{port}{url}\""
  22. echo "---------------------------------------------------------"
  23. echo ""
  24. {wrk} {headers} -d 5 -c 8 -t 8 "http://{server_host}:{port}{url}"
  25. sleep 5
  26. echo ""
  27. echo "---------------------------------------------------------"
  28. echo " Running Warmup {name}"
  29. echo " {wrk} {headers} -d {duration} -c {max_concurrency} -t {max_threads} \"http://{server_host}:{port}{url}\""
  30. echo "---------------------------------------------------------"
  31. echo ""
  32. {wrk} {headers} -d {duration} -c {max_concurrency} -t {max_threads} "http://{server_host}:{port}{url}"
  33. sleep 5
  34. for c in {interval}
  35. do
  36. echo ""
  37. echo "---------------------------------------------------------"
  38. echo " Concurrency: $c for {name}"
  39. echo " {wrk} {headers} {pipeline} -d {duration} -c $c -t $(($c>{max_threads}?{max_threads}:$c)) \"http://{server_host}:{port}{url}\""
  40. echo "---------------------------------------------------------"
  41. echo ""
  42. {wrk} {headers} {pipeline} -d {duration} -c "$c" -t "$(($c>{max_threads}?{max_threads}:$c))" http://{server_host}:{port}{url}
  43. sleep 2
  44. done
  45. """
  46. query_template = """
  47. echo ""
  48. echo "---------------------------------------------------------"
  49. echo " Running Primer {name}"
  50. echo " wrk {headers} -d 5 -c 8 -t 8 \"http://{server_host}:{port}{url}2\""
  51. echo "---------------------------------------------------------"
  52. echo ""
  53. wrk {headers} -d 5 -c 8 -t 8 "http://{server_host}:{port}{url}2"
  54. sleep 5
  55. echo ""
  56. echo "---------------------------------------------------------"
  57. echo " Running Warmup {name}"
  58. echo " wrk {headers} -d {duration} -c {max_concurrency} -t {max_threads} \"http://{server_host}:{port}{url}2\""
  59. echo "---------------------------------------------------------"
  60. echo ""
  61. wrk {headers} -d {duration} -c {max_concurrency} -t {max_threads} "http://{server_host}:{port}{url}2"
  62. sleep 5
  63. for c in {interval}
  64. do
  65. echo ""
  66. echo "---------------------------------------------------------"
  67. echo " Queries: $c for {name}"
  68. echo " wrk {headers} -d {duration} -c {max_concurrency} -t {max_threads} \"http://{server_host}:{port}{url}$c\""
  69. echo "---------------------------------------------------------"
  70. echo ""
  71. wrk {headers} -d {duration} -c {max_concurrency} -t {max_threads} "http://{server_host}:{port}{url}$c"
  72. sleep 2
  73. done
  74. """
  75. # The sort value is the order in which we represent all the tests. (Mainly helpful for our charts to give the underlying data)
  76. # a consistent ordering even when we add or remove tests. Each test should give a sort value in it's benchmark_config file.
  77. sort = 1000
  78. os = 'linux'
  79. ##########################################################################################
  80. # Public Methods
  81. ##########################################################################################
  82. ############################################################
  83. # start(benchmarker)
  84. # Start the test using it's setup file
  85. ############################################################
  86. def start(self):
  87. return self.setup_module.start(self.benchmarker)
  88. ############################################################
  89. # End start
  90. ############################################################
  91. ############################################################
  92. # stop(benchmarker)
  93. # Stops the test using it's setup file
  94. ############################################################
  95. def stop(self):
  96. return self.setup_module.stop()
  97. ############################################################
  98. # End stop
  99. ############################################################
  100. ############################################################
  101. # verify_urls
  102. # Verifys each of the URLs for this test. THis will sinply
  103. # curl the URL and check for it's return status.
  104. # For each url, a flag will be set on this object for whether
  105. # or not it passed
  106. ############################################################
  107. def verify_urls(self):
  108. # JSON
  109. try:
  110. print "VERIFYING JSON (" + self.json_url + ") ..."
  111. url = self.benchmarker.generate_url(self.json_url, self.port)
  112. subprocess.check_call(["curl", "-f", url])
  113. print ""
  114. self.json_url_passed = True
  115. except (AttributeError, subprocess.CalledProcessError) as e:
  116. self.json_url_passed = False
  117. # DB
  118. try:
  119. print "VERIFYING DB (" + self.db_url + ") ..."
  120. url = self.benchmarker.generate_url(self.db_url, self.port)
  121. subprocess.check_call(["curl", "-f", url])
  122. print ""
  123. self.db_url_passed = True
  124. except (AttributeError, subprocess.CalledProcessError) as e:
  125. self.db_url_passed = False
  126. # Query
  127. try:
  128. print "VERIFYING Query (" + self.query_url + "2) ..."
  129. url = self.benchmarker.generate_url(self.query_url + "2", self.port)
  130. subprocess.check_call(["curl", "-f", url])
  131. print ""
  132. self.query_url_passed = True
  133. except (AttributeError, subprocess.CalledProcessError) as e:
  134. self.query_url_passed = False
  135. # Fortune
  136. try:
  137. print "VERIFYING Fortune (" + self.fortune_url + ") ..."
  138. url = self.benchmarker.generate_url(self.fortune_url, self.port)
  139. subprocess.check_call(["curl", "-f", url])
  140. print ""
  141. self.fortune_url_passed = True
  142. except (AttributeError, subprocess.CalledProcessError) as e:
  143. self.fortune_url_passed = False
  144. # Update
  145. try:
  146. print "VERIFYING Update (" + self.update_url + "2) ..."
  147. url = self.benchmarker.generate_url(self.update_url + "2", self.port)
  148. subprocess.check_call(["curl", "-f", url])
  149. print ""
  150. self.update_url_passed = True
  151. except (AttributeError, subprocess.CalledProcessError) as e:
  152. self.update_url_passed = False
  153. # plaintext
  154. try:
  155. print "VERIFYING Plaintext (" + self.plaintext_url + ") ..."
  156. url = self.benchmarker.generate_url(self.plaintext_url, self.port)
  157. subprocess.check_call(["curl", "-f", url])
  158. print ""
  159. self.plaintext_url_passed = True
  160. except (AttributeError, subprocess.CalledProcessError) as e:
  161. self.plaintext_url_passed = False
  162. ############################################################
  163. # End verify_urls
  164. ############################################################
  165. ############################################################
  166. # contains_type(type)
  167. # true if this test contains an implementation of the given
  168. # test type (json, db, etc.)
  169. ############################################################
  170. def contains_type(self, type):
  171. try:
  172. if type == 'json' and self.json_url != None:
  173. return True
  174. if type == 'db' and self.db_url != None:
  175. return True
  176. if type == 'query' and self.query_url != None:
  177. return True
  178. if type == 'fortune' and self.fortune_url != None:
  179. return True
  180. if type == 'update' and self.update_url != None:
  181. return True
  182. if type == 'plaintext' and self.plaintext_url != None:
  183. return True
  184. except AttributeError:
  185. pass
  186. return False
  187. ############################################################
  188. # End stop
  189. ############################################################
  190. ############################################################
  191. # benchmark
  192. # Runs the benchmark for each type of test that it implements
  193. # JSON/DB/Query.
  194. ############################################################
  195. def benchmark(self):
  196. # JSON
  197. try:
  198. if self.json_url_passed and (self.benchmarker.type == "all" or self.benchmarker.type == "json"):
  199. sys.stdout.write("BENCHMARKING JSON ... ")
  200. sys.stdout.flush()
  201. remote_script = self.__generate_concurrency_script(self.json_url, self.port, self.accept_json)
  202. self.__run_benchmark(remote_script, self.benchmarker.output_file(self.name, 'json'))
  203. results = self.__parse_test('json')
  204. self.benchmarker.report_results(framework=self, test="json", results=results['results'])
  205. print "Complete"
  206. except AttributeError:
  207. pass
  208. # DB
  209. try:
  210. if self.db_url_passed and (self.benchmarker.type == "all" or self.benchmarker.type == "db"):
  211. sys.stdout.write("BENCHMARKING DB ... ")
  212. sys.stdout.flush()
  213. remote_script = self.__generate_concurrency_script(self.db_url, self.port, self.accept_json)
  214. self.__run_benchmark(remote_script, self.benchmarker.output_file(self.name, 'db'))
  215. results = self.__parse_test('db')
  216. self.benchmarker.report_results(framework=self, test="db", results=results['results'])
  217. print "Complete"
  218. except AttributeError:
  219. pass
  220. # Query
  221. try:
  222. if self.query_url_passed and (self.benchmarker.type == "all" or self.benchmarker.type == "query"):
  223. sys.stdout.write("BENCHMARKING Query ... ")
  224. sys.stdout.flush()
  225. remote_script = self.__generate_query_script(self.query_url, self.port, self.accept_json)
  226. self.__run_benchmark(remote_script, self.benchmarker.output_file(self.name, 'query'))
  227. results = self.__parse_test('query')
  228. self.benchmarker.report_results(framework=self, test="query", results=results['results'])
  229. print "Complete"
  230. except AttributeError:
  231. pass
  232. # fortune
  233. try:
  234. if self.fortune_url_passed and (self.benchmarker.type == "all" or self.benchmarker.type == "fortune"):
  235. sys.stdout.write("BENCHMARKING Fortune ... ")
  236. sys.stdout.flush()
  237. remote_script = self.__generate_concurrency_script(self.fortune_url, self.port, self.accept_html)
  238. self.__run_benchmark(remote_script, self.benchmarker.output_file(self.name, 'fortune'))
  239. results = self.__parse_test('fortune')
  240. self.benchmarker.report_results(framework=self, test="fortune", results=results['results'])
  241. print "Complete"
  242. except AttributeError:
  243. pass
  244. # update
  245. try:
  246. if self.update_url_passed and (self.benchmarker.type == "all" or self.benchmarker.type == "update"):
  247. sys.stdout.write("BENCHMARKING Update ... ")
  248. sys.stdout.flush()
  249. remote_script = self.__generate_query_script(self.update_url, self.port, self.accept_json)
  250. self.__run_benchmark(remote_script, self.benchmarker.output_file(self.name, 'update'))
  251. results = self.__parse_test('update')
  252. self.benchmarker.report_results(framework=self, test="update", results=results['results'])
  253. print "Complete"
  254. except AttributeError:
  255. pass
  256. # plaintext
  257. try:
  258. if self.plaintext_url_passed and (self.benchmarker.type == "all" or self.benchmarker.type == "plaintext"):
  259. sys.stdout.write("BENCHMARKING Plaintext ... ")
  260. sys.stdout.flush()
  261. remote_script = self.__generate_concurrency_script(self.plaintext_url, self.port, self.accept_plaintext, wrk_command="wrk-pipeline", intervals=[256,1024,4096,16384], pipeline="--pipeline 16")
  262. self.__run_benchmark(remote_script, self.benchmarker.output_file(self.name, 'plaintext'))
  263. results = self.__parse_test('plaintext')
  264. self.benchmarker.report_results(framework=self, test="plaintext", results=results['results'])
  265. print "Complete"
  266. except AttributeError:
  267. pass
  268. ############################################################
  269. # End benchmark
  270. ############################################################
  271. ############################################################
  272. # parse_all
  273. # Method meant to be run for a given timestamp
  274. ############################################################
  275. def parse_all(self):
  276. # JSON
  277. if os.path.exists(self.benchmarker.output_file(self.name, 'json')):
  278. results = self.__parse_test('json')
  279. self.benchmarker.report_results(framework=self, test="json", results=results['results'])
  280. # DB
  281. if os.path.exists(self.benchmarker.output_file(self.name, 'db')):
  282. results = self.__parse_test('db')
  283. self.benchmarker.report_results(framework=self, test="db", results=results['results'])
  284. # Query
  285. if os.path.exists(self.benchmarker.output_file(self.name, 'query')):
  286. results = self.__parse_test('query')
  287. self.benchmarker.report_results(framework=self, test="query", results=results['results'])
  288. # Fortune
  289. if os.path.exists(self.benchmarker.output_file(self.name, 'fortune')):
  290. results = self.__parse_test('fortune')
  291. self.benchmarker.report_results(framework=self, test="fortune", results=results['results'])
  292. # Update
  293. if os.path.exists(self.benchmarker.output_file(self.name, 'update')):
  294. results = self.__parse_test('update')
  295. self.benchmarker.report_results(framework=self, test="update", results=results['results'])
  296. # Plaintext
  297. if os.path.exists(self.benchmarker.output_file(self.name, 'plaintext')):
  298. results = self.__parse_test('plaintext')
  299. self.benchmarker.report_results(framework=self, test="plaintext", results=results['results'])
  300. ############################################################
  301. # End parse_all
  302. ############################################################
  303. ############################################################
  304. # __parse_test(test_type)
  305. ############################################################
  306. def __parse_test(self, test_type):
  307. try:
  308. results = dict()
  309. results['results'] = []
  310. with open(self.benchmarker.output_file(self.name, test_type)) as raw_data:
  311. is_warmup = True
  312. rawData = None
  313. for line in raw_data:
  314. if "Queries:" in line or "Concurrency:" in line:
  315. is_warmup = False
  316. rawData = None
  317. continue
  318. if "Warmup" in line or "Primer" in line:
  319. is_warmup = True
  320. continue
  321. if not is_warmup:
  322. if rawData == None:
  323. rawData = dict()
  324. results['results'].append(rawData)
  325. #if "Requests/sec:" in line:
  326. # m = re.search("Requests/sec:\s+([0-9]+)", line)
  327. # rawData['reportedResults'] = m.group(1)
  328. # search for weighttp data such as succeeded and failed.
  329. if "Latency" in line:
  330. m = re.findall("([0-9]+\.*[0-9]*[us|ms|s|m|%]+)", line)
  331. if len(m) == 4:
  332. rawData['latencyAvg'] = m[0]
  333. rawData['latencyStdev'] = m[1]
  334. rawData['latencyMax'] = m[2]
  335. # rawData['latencyStdevPercent'] = m[3]
  336. #if "Req/Sec" in line:
  337. # m = re.findall("([0-9]+\.*[0-9]*[k|%]*)", line)
  338. # if len(m) == 4:
  339. # rawData['requestsAvg'] = m[0]
  340. # rawData['requestsStdev'] = m[1]
  341. # rawData['requestsMax'] = m[2]
  342. # rawData['requestsStdevPercent'] = m[3]
  343. #if "requests in" in line:
  344. # m = re.search("requests in ([0-9]+\.*[0-9]*[ms|s|m|h]+)", line)
  345. # if m != None:
  346. # # parse out the raw time, which may be in minutes or seconds
  347. # raw_time = m.group(1)
  348. # if "ms" in raw_time:
  349. # rawData['total_time'] = float(raw_time[:len(raw_time)-2]) / 1000.0
  350. # elif "s" in raw_time:
  351. # rawData['total_time'] = float(raw_time[:len(raw_time)-1])
  352. # elif "m" in raw_time:
  353. # rawData['total_time'] = float(raw_time[:len(raw_time)-1]) * 60.0
  354. # elif "h" in raw_time:
  355. # rawData['total_time'] = float(raw_time[:len(raw_time)-1]) * 3600.0
  356. if "requests in" in line:
  357. m = re.search("([0-9]+) requests in", line)
  358. if m != None:
  359. rawData['totalRequests'] = int(m.group(1))
  360. if "Socket errors" in line:
  361. if "connect" in line:
  362. m = re.search("connect ([0-9]+)", line)
  363. rawData['connect'] = int(m.group(1))
  364. if "read" in line:
  365. m = re.search("read ([0-9]+)", line)
  366. rawData['read'] = int(m.group(1))
  367. if "write" in line:
  368. m = re.search("write ([0-9]+)", line)
  369. rawData['write'] = int(m.group(1))
  370. if "timeout" in line:
  371. m = re.search("timeout ([0-9]+)", line)
  372. rawData['timeout'] = int(m.group(1))
  373. if "Non-2xx" in line:
  374. m = re.search("Non-2xx or 3xx responses: ([0-9]+)", line)
  375. if m != None:
  376. rawData['5xx'] = int(m.group(1))
  377. return results
  378. except IOError:
  379. return None
  380. ############################################################
  381. # End benchmark
  382. ############################################################
  383. ##########################################################################################
  384. # Private Methods
  385. ##########################################################################################
  386. ############################################################
  387. # __run_benchmark(script, output_file)
  388. # Runs a single benchmark using the script which is a bash
  389. # template that uses weighttp to run the test. All the results
  390. # outputed to the output_file.
  391. ############################################################
  392. def __run_benchmark(self, script, output_file):
  393. with open(output_file, 'w') as raw_file:
  394. p = subprocess.Popen(self.benchmarker.ssh_string.split(" "), stdin=subprocess.PIPE, stdout=raw_file, stderr=raw_file)
  395. p.communicate(script)
  396. ############################################################
  397. # End __run_benchmark
  398. ############################################################
  399. ############################################################
  400. # __generate_concurrency_script(url, port)
  401. # Generates the string containing the bash script that will
  402. # be run on the client to benchmark a single test. This
  403. # specifically works for the variable concurrency tests (JSON
  404. # and DB)
  405. ############################################################
  406. def __generate_concurrency_script(self, url, port, accept_header, wrk_command="wrk", intervals=[], pipeline=""):
  407. if len(intervals) == 0:
  408. intervals = self.benchmarker.concurrency_levels
  409. headers = self.__get_request_headers(accept_header)
  410. return self.concurrency_template.format(max_concurrency=self.benchmarker.max_concurrency,
  411. max_threads=self.benchmarker.max_threads, name=self.name, duration=self.benchmarker.duration,
  412. interval=" ".join("{}".format(item) for item in intervals),
  413. server_host=self.benchmarker.server_host, port=port, url=url, headers=headers, wrk=wrk_command,
  414. pipeline=pipeline)
  415. ############################################################
  416. # End __generate_concurrency_script
  417. ############################################################
  418. ############################################################
  419. # __generate_query_script(url, port)
  420. # Generates the string containing the bash script that will
  421. # be run on the client to benchmark a single test. This
  422. # specifically works for the variable query tests (Query)
  423. ############################################################
  424. def __generate_query_script(self, url, port, accept_header):
  425. headers = self.__get_request_headers(accept_header)
  426. return self.query_template.format(max_concurrency=self.benchmarker.max_concurrency,
  427. max_threads=self.benchmarker.max_threads, name=self.name, duration=self.benchmarker.duration,
  428. interval=" ".join("{}".format(item) for item in self.benchmarker.query_intervals),
  429. server_host=self.benchmarker.server_host, port=port, url=url, headers=headers)
  430. ############################################################
  431. # End __generate_query_script
  432. ############################################################
  433. ############################################################
  434. # __get_request_headers(accept_header)
  435. # Generates the complete HTTP header string
  436. ############################################################
  437. def __get_request_headers(self, accept_header):
  438. return self.headers_template.format(accept=accept_header)
  439. ############################################################
  440. # End __format_request_headers
  441. ############################################################
  442. ##########################################################################################
  443. # Constructor
  444. ##########################################################################################
  445. def __init__(self, name, directory, benchmarker, args):
  446. self.name = name
  447. self.directory = directory
  448. self.benchmarker = benchmarker
  449. self.__dict__.update(args)
  450. # ensure diretory has __init__.py file so that we can use it as a pythong package
  451. if not os.path.exists(os.path.join(directory, "__init__.py")):
  452. open(os.path.join(directory, "__init__.py"), 'w').close()
  453. self.setup_module = setup_module = importlib.import_module(directory + '.' + self.setup_file)
  454. ############################################################
  455. # End __init__
  456. ############################################################
  457. ############################################################
  458. # End FrameworkTest
  459. ############################################################
  460. ##########################################################################################
  461. # Static methods
  462. ##########################################################################################
  463. ##############################################################
  464. # parse_config(config, directory, benchmarker)
  465. # parses a config file and returns a list of FrameworkTest
  466. # objects based on that config file.
  467. ##############################################################
  468. def parse_config(config, directory, benchmarker):
  469. tests = []
  470. # The config object can specify multiple tests, we neep to loop
  471. # over them and parse them out
  472. for test in config['tests']:
  473. for key, value in test.iteritems():
  474. test_name = config['framework']
  475. # if the test uses the 'defualt' keywork, then we don't
  476. # append anything to it's name. All configs should only have 1 default
  477. if key != 'default':
  478. # we need to use the key in the test_name
  479. test_name = test_name + "-" + key
  480. tests.append(FrameworkTest(test_name, directory, benchmarker, value))
  481. return tests
  482. ##############################################################
  483. # End parse_config
  484. ##############################################################