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