1"""CherryPy Benchmark Tool 2 3 Usage: 4 benchmark.py [options] 5 6 --null: use a null Request object (to bench the HTTP server only) 7 --notests: start the server but do not run the tests; this allows 8 you to check the tested pages with a browser 9 --help: show this help message 10 --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) 11 --modpython: run tests via apache on 54583 (with modpython_gateway) 12 --ab=path: Use the ab script/executable at 'path' (see below) 13 --apache=path: Use the apache script/exe at 'path' (see below) 14 15 To run the benchmarks, the Apache Benchmark tool "ab" must either be on 16 your system path, or specified via the --ab=path option. 17 18 To run the modpython tests, the "apache" executable or script must be 19 on your system path, or provided via the --apache=path option. On some 20 platforms, "apache" may be called "apachectl" or "apache2ctl"--create 21 a symlink to them if needed. 22""" 23 24import getopt 25import os 26import re 27import sys 28import time 29 30import cherrypy 31from cherrypy import _cperror, _cpmodpy 32from cherrypy.lib import httputil 33 34 35curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) 36 37AB_PATH = '' 38APACHE_PATH = 'apache' 39SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog' 40 41__all__ = ['ABSession', 'Root', 'print_report', 42 'run_standard_benchmarks', 'safe_threads', 43 'size_report', 'thread_report', 44 ] 45 46size_cache = {} 47 48 49class Root: 50 51 @cherrypy.expose 52 def index(self): 53 return """<html> 54<head> 55 <title>CherryPy Benchmark</title> 56</head> 57<body> 58 <ul> 59 <li><a href="hello">Hello, world! (14 byte dynamic)</a></li> 60 <li><a href="static/index.html">Static file (14 bytes static)</a></li> 61 <li><form action="sizer">Response of length: 62 <input type='text' name='size' value='10' /></form> 63 </li> 64 </ul> 65</body> 66</html>""" 67 68 @cherrypy.expose 69 def hello(self): 70 return 'Hello, world\r\n' 71 72 @cherrypy.expose 73 def sizer(self, size): 74 resp = size_cache.get(size, None) 75 if resp is None: 76 size_cache[size] = resp = 'X' * int(size) 77 return resp 78 79 80def init(): 81 82 cherrypy.config.update({ 83 'log.error.file': '', 84 'environment': 'production', 85 'server.socket_host': '127.0.0.1', 86 'server.socket_port': 54583, 87 'server.max_request_header_size': 0, 88 'server.max_request_body_size': 0, 89 }) 90 91 # Cheat mode on ;) 92 del cherrypy.config['tools.log_tracebacks.on'] 93 del cherrypy.config['tools.log_headers.on'] 94 del cherrypy.config['tools.trailing_slash.on'] 95 96 appconf = { 97 '/static': { 98 'tools.staticdir.on': True, 99 'tools.staticdir.dir': 'static', 100 'tools.staticdir.root': curdir, 101 }, 102 } 103 globals().update( 104 app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf), 105 ) 106 107 108class NullRequest: 109 110 """A null HTTP request class, returning 200 and an empty body.""" 111 112 def __init__(self, local, remote, scheme='http'): 113 pass 114 115 def close(self): 116 pass 117 118 def run(self, method, path, query_string, protocol, headers, rfile): 119 cherrypy.response.status = '200 OK' 120 cherrypy.response.header_list = [('Content-Type', 'text/html'), 121 ('Server', 'Null CherryPy'), 122 ('Date', httputil.HTTPDate()), 123 ('Content-Length', '0'), 124 ] 125 cherrypy.response.body = [''] 126 return cherrypy.response 127 128 129class NullResponse: 130 pass 131 132 133class ABSession: 134 135 """A session of 'ab', the Apache HTTP server benchmarking tool. 136 137Example output from ab: 138 139This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 140Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 141Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ 142 143Benchmarking 127.0.0.1 (be patient) 144Completed 100 requests 145Completed 200 requests 146Completed 300 requests 147Completed 400 requests 148Completed 500 requests 149Completed 600 requests 150Completed 700 requests 151Completed 800 requests 152Completed 900 requests 153 154 155Server Software: CherryPy/3.1beta 156Server Hostname: 127.0.0.1 157Server Port: 54583 158 159Document Path: /static/index.html 160Document Length: 14 bytes 161 162Concurrency Level: 10 163Time taken for tests: 9.643867 seconds 164Complete requests: 1000 165Failed requests: 0 166Write errors: 0 167Total transferred: 189000 bytes 168HTML transferred: 14000 bytes 169Requests per second: 103.69 [#/sec] (mean) 170Time per request: 96.439 [ms] (mean) 171Time per request: 9.644 [ms] (mean, across all concurrent requests) 172Transfer rate: 19.08 [Kbytes/sec] received 173 174Connection Times (ms) 175 min mean[+/-sd] median max 176Connect: 0 0 2.9 0 10 177Processing: 20 94 7.3 90 130 178Waiting: 0 43 28.1 40 100 179Total: 20 95 7.3 100 130 180 181Percentage of the requests served within a certain time (ms) 182 50% 100 183 66% 100 184 75% 100 185 80% 100 186 90% 100 187 95% 100 188 98% 100 189 99% 110 190 100% 130 (longest request) 191Finished 1000 requests 192""" 193 194 parse_patterns = [ 195 ('complete_requests', 'Completed', 196 br'^Complete requests:\s*(\d+)'), 197 ('failed_requests', 'Failed', 198 br'^Failed requests:\s*(\d+)'), 199 ('requests_per_second', 'req/sec', 200 br'^Requests per second:\s*([0-9.]+)'), 201 ('time_per_request_concurrent', 'msec/req', 202 br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'), 203 ('transfer_rate', 'KB/sec', 204 br'^Transfer rate:\s*([0-9.]+)') 205 ] 206 207 def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000, 208 concurrency=10): 209 self.path = path 210 self.requests = requests 211 self.concurrency = concurrency 212 213 def args(self): 214 port = cherrypy.server.socket_port 215 assert self.concurrency > 0 216 assert self.requests > 0 217 # Don't use "localhost". 218 # Cf 219 # http://mail.python.org/pipermail/python-win32/2008-March/007050.html 220 return ('-k -n %s -c %s http://127.0.0.1:%s%s' % 221 (self.requests, self.concurrency, port, self.path)) 222 223 def run(self): 224 # Parse output of ab, setting attributes on self 225 try: 226 self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args()) 227 except Exception: 228 print(_cperror.format_exc()) 229 raise 230 231 for attr, name, pattern in self.parse_patterns: 232 val = re.search(pattern, self.output, re.MULTILINE) 233 if val: 234 val = val.group(1) 235 setattr(self, attr, val) 236 else: 237 setattr(self, attr, None) 238 239 240safe_threads = (25, 50, 100, 200, 400) 241if sys.platform in ('win32',): 242 # For some reason, ab crashes with > 50 threads on my Win2k laptop. 243 safe_threads = (10, 20, 30, 40, 50) 244 245 246def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads): 247 sess = ABSession(path) 248 attrs, names, patterns = list(zip(*sess.parse_patterns)) 249 avg = dict.fromkeys(attrs, 0.0) 250 251 yield ('threads',) + names 252 for c in concurrency: 253 sess.concurrency = c 254 sess.run() 255 row = [c] 256 for attr in attrs: 257 val = getattr(sess, attr) 258 if val is None: 259 print(sess.output) 260 row = None 261 break 262 val = float(val) 263 avg[attr] += float(val) 264 row.append(val) 265 if row: 266 yield row 267 268 # Add a row of averages. 269 yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs] 270 271 272def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), 273 concurrency=50): 274 sess = ABSession(concurrency=concurrency) 275 attrs, names, patterns = list(zip(*sess.parse_patterns)) 276 yield ('bytes',) + names 277 for sz in sizes: 278 sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz) 279 sess.run() 280 yield [sz] + [getattr(sess, attr) for attr in attrs] 281 282 283def print_report(rows): 284 for row in rows: 285 print('') 286 for val in row: 287 sys.stdout.write(str(val).rjust(10) + ' | ') 288 print('') 289 290 291def run_standard_benchmarks(): 292 print('') 293 print('Client Thread Report (1000 requests, 14 byte response body, ' 294 '%s server threads):' % cherrypy.server.thread_pool) 295 print_report(thread_report()) 296 297 print('') 298 print('Client Thread Report (1000 requests, 14 bytes via staticdir, ' 299 '%s server threads):' % cherrypy.server.thread_pool) 300 print_report(thread_report('%s/static/index.html' % SCRIPT_NAME)) 301 302 print('') 303 print('Size Report (1000 requests, 50 client threads, ' 304 '%s server threads):' % cherrypy.server.thread_pool) 305 print_report(size_report()) 306 307 308# modpython and other WSGI # 309 310def startup_modpython(req=None): 311 """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI). 312 """ 313 if cherrypy.engine.state == cherrypy._cpengine.STOPPED: 314 if req: 315 if 'nullreq' in req.get_options(): 316 cherrypy.engine.request_class = NullRequest 317 cherrypy.engine.response_class = NullResponse 318 ab_opt = req.get_options().get('ab', '') 319 if ab_opt: 320 global AB_PATH 321 AB_PATH = ab_opt 322 cherrypy.engine.start() 323 if cherrypy.engine.state == cherrypy._cpengine.STARTING: 324 cherrypy.engine.wait() 325 return 0 # apache.OK 326 327 328def run_modpython(use_wsgi=False): 329 print('Starting mod_python...') 330 pyopts = [] 331 332 # Pass the null and ab=path options through Apache 333 if '--null' in opts: 334 pyopts.append(('nullreq', '')) 335 336 if '--ab' in opts: 337 pyopts.append(('ab', opts['--ab'])) 338 339 s = _cpmodpy.ModPythonServer 340 if use_wsgi: 341 pyopts.append(('wsgi.application', 'cherrypy::tree')) 342 pyopts.append( 343 ('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython')) 344 handler = 'modpython_gateway::handler' 345 s = s(port=54583, opts=pyopts, 346 apache_path=APACHE_PATH, handler=handler) 347 else: 348 pyopts.append( 349 ('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython')) 350 s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) 351 352 try: 353 s.start() 354 run() 355 finally: 356 s.stop() 357 358 359if __name__ == '__main__': 360 init() 361 362 longopts = ['cpmodpy', 'modpython', 'null', 'notests', 363 'help', 'ab=', 'apache='] 364 try: 365 switches, args = getopt.getopt(sys.argv[1:], '', longopts) 366 opts = dict(switches) 367 except getopt.GetoptError: 368 print(__doc__) 369 sys.exit(2) 370 371 if '--help' in opts: 372 print(__doc__) 373 sys.exit(0) 374 375 if '--ab' in opts: 376 AB_PATH = opts['--ab'] 377 378 if '--notests' in opts: 379 # Return without stopping the server, so that the pages 380 # can be tested from a standard web browser. 381 def run(): 382 port = cherrypy.server.socket_port 383 print('You may now open http://127.0.0.1:%s%s/' % 384 (port, SCRIPT_NAME)) 385 386 if '--null' in opts: 387 print('Using null Request object') 388 else: 389 def run(): 390 end = time.time() - start 391 print('Started in %s seconds' % end) 392 if '--null' in opts: 393 print('\nUsing null Request object') 394 try: 395 try: 396 run_standard_benchmarks() 397 except Exception: 398 print(_cperror.format_exc()) 399 raise 400 finally: 401 cherrypy.engine.exit() 402 403 print('Starting CherryPy app server...') 404 405 class NullWriter(object): 406 407 """Suppresses the printing of socket errors.""" 408 409 def write(self, data): 410 pass 411 sys.stderr = NullWriter() 412 413 start = time.time() 414 415 if '--cpmodpy' in opts: 416 run_modpython() 417 elif '--modpython' in opts: 418 run_modpython(use_wsgi=True) 419 else: 420 if '--null' in opts: 421 cherrypy.server.request_class = NullRequest 422 cherrypy.server.response_class = NullResponse 423 424 cherrypy.engine.start_with_callback(run) 425 cherrypy.engine.block() 426