2 # $Id: bench.py 977 2012-01-22 23:05:09Z g.rodola $
4 # pyftpdlib is released under the MIT license, reproduced below:
5 # ======================================================================
6 # Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com>
10 # Permission is hereby granted, free of charge, to any person
11 # obtaining a copy of this software and associated documentation
12 # files (the "Software"), to deal in the Software without
13 # restriction, including without limitation the rights to use,
14 # copy, modify, merge, publish, distribute, sublicense, and/or sell
15 # copies of the Software, and to permit persons to whom the
16 # Software is furnished to do so, subject to the following
19 # The above copyright notice and this permission notice shall be
20 # included in all copies or substantial portions of the Software.
22 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 # OTHER DEALINGS IN THE SOFTWARE.
31 # ======================================================================
34 FTP server benchmark script.
36 In order to run this you must have a listening FTP server with a user
37 with writing permissions configured.
40 python bench.py -u USER -p PASSWORD
41 python bench.py -u USER -p PASSWORD -H 127.0.0.1 -P 21
42 python bench.py -u USER -p PASSWORD -b transfer
43 python bench.py -u USER -p PASSWORD -b concurrence
44 python bench.py -u USER -p PASSWORD -b all
45 python bench.py -u USER -p PASSWORD -b concurrence -n 500
48 # Some benchmarks (Linux 3.0.0, Intel core duo - 3.1 Ghz).
52 # STOR (client -> server) 512.70 MB/sec
53 # RETR (server -> client) 689.49 MB/sec
54 # 200 concurrent clients (connect, login) 0.26 secs
55 # 200 concurrent clients (RETR 10M file) 3.24 secs
56 # 200 concurrent clients (STOR 10M file) 3.80 secs
57 # 200 concurrent clients (quit) 0.04 secs
61 # STOR (client -> server) 508.80 MB/sec
62 # RETR (server -> client) 1635.14 MB/sec
63 # 200 concurrent clients (connect, login) 0.22 secs
64 # 200 concurrent clients (RETR 10M file) 2.33 secs
65 # 200 concurrent clients (STOR 10M file) 3.83 secs
66 # 200 concurrent clients (quit) 0.02 secs
70 # STOR (client -> server) 609.22 MB/sec
71 # RETR (server -> client) 1313.77 MB/sec
72 # 200 concurrent clients (connect, login) 7.53 secs
73 # 200 concurrent clients (RETR 10M file) 2.76 secs
74 # 200 concurrent clients (STOR 10M file) 6.39 secs
75 # 200 concurrent clients (quit) 0.23 secs
78 from __future__ import with_statement, division
95 TESTFILE_SIZE = 1024 * 1024 * 100 # 100 MB
100 def hilite(string, ok=True, bold=False):
101 """Return an highlighted version of 'string'."""
103 if ok is None: # no color
111 return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string)
113 if not sys.stdout.isatty() or os.name != 'posix':
114 def hilite(s, *args, **kwargs):
117 def print_bench(what, value, unit=""):
118 s = "%s %s %s" % (hilite("%-42s" % what, ok=None, bold=0),
119 hilite("%8.2f" % value),
123 # http://goo.gl/zeJZl
124 def bytes2human(n, format="%(value)i%(symbol)s"):
126 >>> bytes2human(10000)
128 >>> bytes2human(100001221)
131 symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
133 for i, s in enumerate(symbols[1:]):
134 prefix[s] = 1 << (i+1)*10
135 for symbol in reversed(symbols[1:]):
136 if n >= prefix[symbol]:
137 value = float(n) / prefix[symbol]
138 return format % locals()
139 return format % dict(symbol=symbols[0], value=n)
142 """"Utility function for making simple benchmarks (calculates time calls).
143 It can be used either as a context manager or as a decorator.
145 @contextlib.contextmanager
147 timer = time.clock if sys.platform == "win32" else time.time
152 print_bench(what, res, "secs")
154 if hasattr(what,"__call__"):
155 def timed(*args,**kwargs):
157 return what(*args,**kwargs)
163 """Connect to FTP server, login and return an ftplib.FTP instance."""
165 ftp.connect(HOST, PORT)
166 ftp.login(USER, PASSWORD)
170 """Same as ftplib's retrbinary() but discard the received data."""
171 ftp.voidcmd('TYPE I')
172 conn = ftp.transfercmd("RETR " + TESTFN)
175 data = conn.recv(BUFFER_LEN)
178 recv_bytes += len(data)
183 """Same as ftplib's storbinary() but just sends dummy data
184 instead of reading it from a real file.
186 ftp.voidcmd('TYPE I')
187 conn = ftp.transfercmd("STOR " + TESTFN)
188 chunk = 'x' * BUFFER_LEN
191 sent = conn.send(chunk)
193 if total_sent >= size:
198 def bytes_per_second(ftp, retr=True):
199 """Return the number of bytes transmitted in 1 second."""
200 stop_at = time.time() + 1.0
204 ftp.voidcmd('TYPE I')
205 conn = ftp.transfercmd("retr " + TESTFN)
208 conn = request_file()
209 while stop_at > time.time():
210 chunk = conn.recv(BUFFER_LEN)
213 while conn.recv(BUFFER_LEN):
217 conn = request_file()
218 stop_at += time.time() - a
223 except (ftplib.error_temp, ftplib.error_perm):
226 ftp.voidcmd('TYPE I')
227 conn = ftp.transfercmd("STOR " + TESTFN)
228 chunk = 'x' * BUFFER_LEN
229 stop_at = time.time() + 1
230 while stop_at > time.time():
231 bytes += conn.send(chunk)
242 except (ftplib.error_perm, ftplib.error_temp):
247 class AsyncReader(asyncore.dispatcher):
248 """Just read data from a connected socket, asynchronously."""
250 def __init__(self, sock):
251 asyncore.dispatcher.__init__(self, sock)
256 def handle_read(self):
257 chunk = self.socket.recv(BUFFER_LEN)
261 def handle_close(self):
264 def handle_error(self):
267 class AsyncWriter(asynchat.async_chat):
268 """Just write dummy data to a connected socket, asynchronously."""
270 def __init__(self, size):
273 self.chunk = 'x' * BUFFER_LEN
276 if self.sent >= self.size:
278 self.sent += len(self.chunk)
281 def __init__(self, sock, size):
282 asynchat.async_chat.__init__(self, sock)
283 self.push_with_producer(self.ChunkProducer(size))
284 self.close_when_done()
286 def handle_error(self):
289 class AsyncQuit(asynchat.async_chat):
291 def __init__(self, sock):
292 asynchat.async_chat.__init__(self, sock)
294 self.set_terminator('\r\n')
295 self.push('QUIT\r\n')
297 def collect_incoming_data(self, data):
298 self.in_buffer.append(data)
300 def found_terminator(self):
303 def handle_error(self):
306 class OptFormatter(optparse.IndentedHelpFormatter):
308 def format_epilog(self, s):
311 def format_option(self, option):
313 opts = self.option_strings[option]
314 result.append(' %s\n' % opts)
316 help_text = ' %s\n\n' % self.expand_default(option)
317 result.append(help_text)
318 return ''.join(result)
321 global HOST, PORT, USER, PASSWORD
322 USAGE = "%s -u USERNAME -p PASSWORD [-H] [-P] [-b] [-n]" % __file__
323 parser = optparse.OptionParser(usage=USAGE,
324 epilog=__doc__[__doc__.find('Example'):],
325 formatter=OptFormatter())
326 parser.add_option('-u', '--user', dest='user', help='username')
327 parser.add_option('-p', '--pass', dest='password', help='password')
328 parser.add_option('-H', '--host', dest='host', default=HOST, help='hostname')
329 parser.add_option('-P', '--port', dest='port', default=PORT, help='port')
330 parser.add_option('-b', '--benchmark', dest='benchmark', default='transfer',
331 help="benchmark type ('transfer', 'concurrence', 'all')")
332 parser.add_option('-n', '--clients', dest='clients', default=200, type="int",
333 help="number of concurrent clients used by 'concurrence' "
335 options, args = parser.parse_args()
336 if not options.user or not options.password:
340 PASSWORD = options.password
345 bytes = bytes_per_second(connect(), retr=False)
346 print_bench("STOR (client -> server)",
347 round(bytes / 1024.0 / 1024.0, 2), "MB/sec")
350 bytes = bytes_per_second(connect(), retr=True)
351 print_bench("RETR (server -> client)",
352 round(bytes / 1024.0 / 1024.0, 2), "MB/sec")
355 howmany = options.clients
356 FILE_SIZE = 1024 * 1024 * 10 # 10MB
358 def bench_multi_connect():
359 with timethis("%i concurrent clients (connect, login)" % howmany):
361 for x in range(howmany):
362 clients.append(connect())
365 def bench_multi_retr(clients):
366 stor(clients[0], FILE_SIZE)
368 ftp.voidcmd('TYPE I')
369 conn = ftp.transfercmd("RETR " + TESTFN)
371 with timethis("%s concurrent clients (RETR %s file)" \
372 % (howmany, bytes2human(FILE_SIZE))):
373 asyncore.loop(use_poll=True)
377 def bench_multi_stor(clients):
379 ftp.voidcmd('TYPE I')
380 conn = ftp.transfercmd("STOR " + TESTFN)
381 AsyncWriter(conn, 1024 * 1024 * 5)
382 with timethis("%s concurrent clients (STOR %s file)" \
383 % (howmany, bytes2human(FILE_SIZE))):
384 asyncore.loop(use_poll=True)
388 def bench_multi_quit(clients):
391 with timethis("%i concurrent clients (quit)" % howmany):
392 asyncore.loop(use_poll=True)
394 clients = bench_multi_connect()
395 bench_multi_retr(clients)
396 bench_multi_stor(clients)
397 bench_multi_quit(clients)
399 # before starting make sure we have write permissions
401 conn = ftp.transfercmd("STOR " + TESTFN)
406 atexit.register(cleanup)
409 if options.benchmark == 'transfer':
412 elif options.benchmark == 'concurrence':
414 elif options.benchmark == 'all':
419 sys.exit("invalid 'benchmark' parameter %r" % options.benchmark)
421 if __name__ == '__main__':