run-tests.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. #!/usr/bin/env python
  2. import argparse
  3. import ConfigParser
  4. import sys
  5. import os
  6. import multiprocessing
  7. import itertools
  8. import copy
  9. import subprocess
  10. from pprint import pprint
  11. from benchmark.benchmarker import Benchmarker
  12. from setup.linux.unbuffered import Unbuffered
  13. from setup.linux import setup_util
  14. from ast import literal_eval
  15. # Enable cross-platform colored output
  16. from colorama import init
  17. init()
  18. class StoreSeqAction(argparse.Action):
  19. '''Helper class for parsing a sequence from the command line'''
  20. def __init__(self, option_strings, dest, nargs=None, **kwargs):
  21. super(StoreSeqAction, self).__init__(option_strings, dest, type=str, **kwargs)
  22. def __call__(self, parser, namespace, values, option_string=None):
  23. setattr(namespace, self.dest, self.parse_seq(values))
  24. def parse_seq(self, argument):
  25. result = argument.split(',')
  26. sequences = [x for x in result if ":" in x]
  27. for sequence in sequences:
  28. try:
  29. (start,step,end) = sequence.split(':')
  30. except ValueError:
  31. print " Invalid: %s" % sequence
  32. print " Requires start:step:end, e.g. 1:2:10"
  33. raise
  34. result.remove(sequence)
  35. result = result + range(int(start), int(end), int(step))
  36. return [abs(int(item)) for item in result]
  37. ###################################################################################################
  38. # Main
  39. ###################################################################################################
  40. def main(argv=None):
  41. ''' Runs the program. There are three ways to pass arguments
  42. 1) environment variables TFB_*
  43. 2) configuration file benchmark.cfg
  44. 3) command line flags
  45. In terms of precedence, 3 > 2 > 1, so config file trumps environment variables
  46. but command line flags have the final say
  47. '''
  48. # Do argv default this way, as doing it in the functional declaration sets it at compile time
  49. if argv is None:
  50. argv = sys.argv
  51. # Enable unbuffered output so messages will appear in the proper order with subprocess output.
  52. sys.stdout=Unbuffered(sys.stdout)
  53. # Update python environment
  54. # 1) Ensure the current directory (which should be the benchmark home directory) is in the path so that the tests can be imported.
  55. sys.path.append('.')
  56. # 2) Ensure toolset/setup/linux is in the path so that the tests can "import setup_util".
  57. sys.path.append('toolset/setup/linux')
  58. # Update environment for shell scripts
  59. fwroot = setup_util.get_fwroot()
  60. if not fwroot:
  61. fwroot = os.getcwd()
  62. setup_util.replace_environ(config='config/benchmark_profile', root=fwroot)
  63. print "FWROOT is %s"%setup_util.get_fwroot()
  64. conf_parser = argparse.ArgumentParser(
  65. description=__doc__,
  66. formatter_class=argparse.RawDescriptionHelpFormatter,
  67. add_help=False)
  68. conf_parser.add_argument('--conf_file', default='benchmark.cfg', metavar='FILE', help='Optional configuration file to provide argument defaults. All config options can be overridden using the command line.')
  69. args, remaining_argv = conf_parser.parse_known_args()
  70. try:
  71. with open (args.conf_file):
  72. config = ConfigParser.SafeConfigParser()
  73. config.read([os.getcwd() + '/' + args.conf_file])
  74. defaults = dict(config.items("Defaults"))
  75. # Convert strings into proper python types
  76. for k,v in defaults.iteritems():
  77. try:
  78. defaults[k] = literal_eval(v)
  79. except Exception:
  80. pass
  81. except IOError:
  82. if args.conf_file != 'benchmark.cfg':
  83. print 'Configuration file not found!'
  84. defaults = { "client-host":"localhost"}
  85. ##########################################################
  86. # Set up default values
  87. ##########################################################
  88. serverHost = os.environ.get('TFB_SERVER_HOST')
  89. clientHost = os.environ.get('TFB_CLIENT_HOST')
  90. clientUser = os.environ.get('TFB_CLIENT_USER')
  91. clientIden = os.environ.get('TFB_CLIENT_IDENTITY_FILE')
  92. runnerUser = os.environ.get('TFB_RUNNER_USER')
  93. databaHost = os.getenv('TFB_DATABASE_HOST', clientHost)
  94. databaUser = os.getenv('TFB_DATABASE_USER', clientUser)
  95. dbIdenFile = os.getenv('TFB_DATABASE_IDENTITY_FILE', clientIden)
  96. maxThreads = 8
  97. try:
  98. maxThreads = multiprocessing.cpu_count()
  99. except Exception:
  100. pass
  101. ##########################################################
  102. # Set up argument parser
  103. ##########################################################
  104. parser = argparse.ArgumentParser(description="Install or run the Framework Benchmarks test suite.",
  105. parents=[conf_parser],
  106. formatter_class=argparse.ArgumentDefaultsHelpFormatter,
  107. epilog='''If an argument includes (type int-sequence), then it accepts integer lists in multiple forms.
  108. Using a single number e.g. 5 will create a list [5]. Using commas will create a list containing those
  109. values e.g. 1,3,6 creates [1, 3, 6]. Using three colon-separated numbers of start:step:end will create a
  110. list, using the semantics of python's range function, e.g. 1:3:15 creates [1, 4, 7, 10, 13] while
  111. 0:1:5 creates [0, 1, 2, 3, 4]
  112. ''')
  113. # SSH options
  114. parser.add_argument('-s', '--server-host', default=serverHost, help='The application server.')
  115. parser.add_argument('-c', '--client-host', default=clientHost, help='The client / load generation server.')
  116. parser.add_argument('-u', '--client-user', default=clientUser, help='The username to use for SSH to the client instance.')
  117. parser.add_argument('-i', '--client-identity-file', dest='client_identity_file', default=clientIden,
  118. help='The key to use for SSH to the client instance.')
  119. parser.add_argument('-d', '--database-host', default=databaHost,
  120. help='The database server. If not provided, defaults to the value of --client-host.')
  121. parser.add_argument('--database-user', default=databaUser,
  122. help='The username to use for SSH to the database instance. If not provided, defaults to the value of --client-user.')
  123. parser.add_argument('--database-identity-file', default=dbIdenFile, dest='database_identity_file',
  124. help='The key to use for SSH to the database instance. If not provided, defaults to the value of --client-identity-file.')
  125. parser.add_argument('-p', dest='password_prompt', action='store_true', help='Prompt for password')
  126. # Install options
  127. parser.add_argument('--install', choices=['client', 'database', 'server', 'all'], default=None,
  128. help='Runs installation script(s) before continuing on to execute the tests.')
  129. parser.add_argument('--install-error-action', choices=['abort', 'continue'], default='continue', help='action to take in case of error during installation')
  130. parser.add_argument('--install-strategy', choices=['unified', 'pertest'], default='unified',
  131. help='''Affects : With unified, all server software is installed into a single directory.
  132. With pertest each test gets its own installs directory, but installation takes longer''')
  133. parser.add_argument('--install-only', action='store_true', default=False, help='Do not run benchmark or verification, just install and exit')
  134. parser.add_argument('--clean', action='store_true', default=False, help='Removes the results directory')
  135. parser.add_argument('--clean-all', action='store_true', dest='clean_all', default=False, help='Removes the results and installs directories')
  136. # Test options
  137. parser.add_argument('--test', nargs='+', help='names of tests to run')
  138. parser.add_argument('--exclude', nargs='+', help='names of tests to exclude')
  139. parser.add_argument('--type', choices=['all', 'json', 'db', 'query', 'fortune', 'update', 'plaintext'], default='all', help='which type of test to run')
  140. parser.add_argument('-m', '--mode', choices=['benchmark', 'verify'], default='benchmark', help='verify mode will only start up the tests, curl the urls and shutdown')
  141. parser.add_argument('--list-tests', action='store_true', default=False, help='lists all the known tests that can run')
  142. parser.add_argument('--list-test-metadata', action='store_true', default=False, help='writes all the test metadata as a JSON file in the results directory')
  143. parser.add_argument('--os', choices=['linux', 'windows'], default='linux', help='The operating system of the application/framework server (the one running' +
  144. 'this binary')
  145. parser.add_argument('--database-os', choices=['linux', 'windows'], default='linux', help='The operating system of the database server.')
  146. # Benchmark options
  147. parser.add_argument('--concurrency-levels', default=[8, 16, 32, 64, 128, 256], help='Runs wrk benchmarker with different concurrency value (type int-sequence)', action=StoreSeqAction)
  148. parser.add_argument('--query-levels', default=[1, 5,10,15,20], help='Database queries requested per HTTP connection, used during query test (type int-sequence)', action=StoreSeqAction)
  149. parser.add_argument('--threads', default=maxThreads, help='Run wrk benchmarker with this many threads. This should probably be the number of cores for your client system', type=int)
  150. parser.add_argument('--duration', default=15, help='Time in seconds that each test should run for.')
  151. parser.add_argument('--sleep', type=int, default=60, help='the amount of time to sleep after starting each test to allow the server to start up.')
  152. # Misc Options
  153. parser.add_argument('--parse', help='Parses the results of the given timestamp and merges that with the latest results')
  154. parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Causes the configuration to print before any other commands are executed.')
  155. parser.add_argument('--clear-tmp', action='store_true', default=False, help='Clears files written to /tmp after each framework\'s tests complete.')
  156. parser.set_defaults(**defaults) # Must do this after add, or each option's default will override the configuration file default
  157. args = parser.parse_args(remaining_argv)
  158. # Verify and massage options
  159. if args.client_user is None:
  160. print 'Usernames (e.g. --client-user, and --database-user) are required!'
  161. print 'The system will SSH into the client and the database for the install stage'
  162. print 'Aborting'
  163. exit(1)
  164. if args.database_user is None:
  165. args.database_user = args.client_user
  166. if args.database_host is None:
  167. args.database_host = args.client_host
  168. if args.verbose:
  169. print 'Configuration options: '
  170. pprint(vars(args))
  171. benchmarker = Benchmarker(vars(args))
  172. # Run the benchmarker in the specified mode
  173. # Do not use benchmarker variables for these checks,
  174. # they are either str or bool based on the python version
  175. if args.list_tests:
  176. benchmarker.run_list_tests()
  177. elif args.list_test_metadata:
  178. benchmarker.run_list_test_metadata()
  179. elif args.parse != None:
  180. benchmarker.parse_timestamp()
  181. elif not args.install_only:
  182. return benchmarker.run()
  183. if __name__ == "__main__":
  184. sys.exit(main())