networking: interface raw define batch size for adding rx descriptors
[barrelfish] / tools / harness / tests / tftp.py
1 ##########################################################################
2 # Copyright (c) 2009, ETH Zurich.
3 # All rights reserved.
4 #
5 # This file is distributed under the terms in the attached LICENSE file.
6 # If you do not find this file, copies can be found by writing to:
7 # ETH Zurich D-INFK, Haldeneggsteig 4, CH-8092 Zurich. Attn: Systems Group.
8 ##########################################################################
9
10 import re, socket, httplib, traceback, os, subprocess, datetime, glob, time
11 import tests, debug, siteconfig
12 from common import TestCommon, TimeoutError, select_timeout
13 from results import ResultsBase, PassFailResult, RowResults
14
15
16 WEBSERVER_TEST_FILES=['index.html', 'barrelfish.gif', 'barrelfish_sosp09.pdf', 'nevill-master-capabilities.pdf', 'razavi-master-performanceisolation.pdf']
17
18 WEBSERVER_TIMEOUT=5 # seconds
19 TEST_LOG_NAME = 'testlog.txt'
20
21 HTTPERF_BASE_ARGS='--hog --close-with-reset --timeout 2 '
22 HTTPERF_URI = '/index.html'
23
24 # Webserver stress test, It will download index page repeatedly for following
25 #       number of times
26 WEBSERVER_STRESS_COUNTER = 3000
27
28 # desired duration of an httperf test run (seconds)
29 HTTPERF_DURATION = 20
30
31 # sleep time between runs (seconds)
32 HTTPERF_SLEEPTIME = 20
33
34 # timeout for a complete run, including setup etc.
35 HTTPERF_TIMEOUT = datetime.timedelta(seconds=(HTTPERF_DURATION + 30))
36
37 # connection rates across all client machines
38 HTTPERF_STARTRATE = 1000  # initial rate
39 HTTPERF_RATEINCREMENT = 1000  # amount to increment by for each new run
40
41
42 class WebCommon(TestCommon):
43
44     def __init__(self, options):
45         super(WebCommon, self).__init__(options)
46         self.test_timeout_delta = datetime.timedelta(seconds=600)
47         self.read_after_finished = True
48         self.server_failures = []
49
50     def setup(self, build, machine, testdir):
51         super(WebCommon, self).setup(build, machine, testdir)
52         self.testdir = testdir
53         self.finished = False
54         self.ip = None
55
56     def get_modules(self, build, machine):
57         cardName = "e1000"
58         modules = super(WebCommon, self).get_modules(build, machine)
59         modules.add_module("e1000n", ["auto"])
60         modules.add_module("NGD_mng", ["auto"])
61         modules.add_module("netd", ["auto"])
62         nfsip = socket.gethostbyname(siteconfig.get('WEBSERVER_NFS_HOST'))
63         modules.add_module("webserver", ["core=%d" % machine.get_coreids()[0], #2
64                                 cardName, nfsip,
65                                          siteconfig.get('WEBSERVER_NFS_PATH')])
66 #                                         siteconfig.get('WEBSERVER_NFS_TEST_PATH')])
67         return modules
68
69     def process_line(self, line):
70         m = re.match(r'Interface up! IP address (\d+\.\d+\.\d+\.\d+)', line)
71         if m:
72             self.ip = m.group(1)
73         elif self.ip and 'Starting webserver' in line:
74             debug.verbose("Running the tests")
75             self.runtests(self.ip)
76             self.finished = True
77         elif line.startswith("kernel PANIC!") or \
78              line.startswith("Assertion failed on core") or \
79              re.match("Assertion .* failed at line", line) or \
80              line.startswith("Aborted"):
81             # Severe error in webserver, failing test
82             if line.startswith("Aborted") and \
83                self.previous_line not in self.server_failures:
84                 line = self.previous_line
85             self.server_failures.append(line.strip())
86             self.finished = True
87
88         self.previous_line = line.strip()
89
90     def passed(self):
91         return len(self.server_failures) == 0
92
93     def is_finished(self, line):
94         return self.finished
95
96
97 @tests.add_test
98 class WebserverTest(WebCommon):
99     '''tests webserver functionality'''
100     name = "webserver"
101
102     def setup(self, *args):
103         super(WebserverTest, self).setup(*args)
104         self.testlog = None
105
106     def getpage_stress(self, server, page, count):
107         debug.verbose('requesting http://%s/%s' % (server, page))
108         failure_count = 0;
109         #c = httplib.HTTPConnection(server, timeout=WEBSERVER_TIMEOUT)
110         for i in range(count):
111             try:
112                 c = httplib.HTTPConnection(server, timeout=WEBSERVER_TIMEOUT)
113                 c.request('GET', '/' + page)
114                 r = c.getresponse()
115                 if (r.status / 100) != 2 :
116                     print "HTTP request failed for %d" % (i)
117                 assert((r.status / 100) == 2) # check for success response
118
119                 # Reset failure count after sucessful retrival
120                 failure_count = 0
121                 c.close()
122             except Exception as e:
123                 print "HTTP request failed for %d, (failure count %d)" % (i,
124                         failure_count)
125                 print "Exception: ", e
126                 failure_count = failure_count + 1
127                 if failure_count >= 3:
128                     print "HTTP request failed for 3 successive times."
129                     print "Giving up for %d, (failure count %d)" % (i,
130                         failure_count)
131                     raise
132
133             #c.close()
134         debug.verbose('server replied %s %s for %d times' % (r.status, r.reason, count))
135
136
137     def getpage(self, server, page):
138         debug.verbose('requesting http://%s/%s' % (server, page))
139         c = httplib.HTTPConnection(server, timeout=WEBSERVER_TIMEOUT)
140         c.request('GET', '/' + page)
141         r = c.getresponse()
142
143         debug.verbose('server replied %s %s' % (r.status, r.reason))
144         assert((r.status / 100) == 2) # check for success response
145
146         try:
147             local_path = siteconfig.get('WEBSERVER_LOCAL_PATH')
148         except AttributeError:
149             local_path = None
150         local = os.path.join(local_path, page) if local_path else None
151         if local and os.path.isfile(local) and os.access(local, os.R_OK):
152             debug.verbose('comparing content to %s' % local)
153             l = open(local, 'r')
154             # read from both files and compare
155             CHUNKSIZE = 4096
156             while True:
157                 remote_data = r.read(CHUNKSIZE)
158                 local_data = l.read(CHUNKSIZE)
159                 if remote_data != local_data:
160                     print "Remote and local data did not match:"
161                     print "Remote data\n"
162                     print remote_data
163                     print "Local data\n"
164                     print local_data
165                 assert(remote_data == local_data)
166                 if len(local_data) < CHUNKSIZE:
167                     break
168
169             debug.verbose('contents matched for %s' % local)
170         c.close()
171
172     def dotest(self, func, args):
173         exception = None
174         r = None
175         try:
176             r = func(*args)
177         except Exception as e:
178             exception = e
179
180         s = 'Test: %s%s\t%s\n' % (func.__name__, str(args),
181                                  'FAIL' if exception else 'PASS')
182         if exception:
183             debug.verbose('Exception while running test: %s\n'
184                           % traceback.format_exc())
185             s += 'Error was: %s\n' % traceback.format_exc()
186         self.testlog.write(s)
187
188         return r
189
190     def runtests(self, server):
191         stress_counter = WEBSERVER_STRESS_COUNTER
192         self.testlog = open(os.path.join(self.testdir, TEST_LOG_NAME), 'w')
193         for f in WEBSERVER_TEST_FILES:
194             self.dotest(self.getpage, (server, f))
195             debug.verbose("Running stresstest: (%d GET %s)" %
196                     (stress_counter, str(f)))
197             self.dotest(self.getpage_stress, (server, f, stress_counter))
198         self.testlog.close()
199
200     def process_data(self, testdir, rawiter):
201         # the test passed iff we see at least one PASS and no FAILs in the log
202         passed = None
203         try:
204             testlog = open(os.path.join(testdir, TEST_LOG_NAME), 'r')
205         except IOError as e:
206             debug.verbose("Cannot find test log, failing test")
207             return PassFailResult(False, reason="Cannot find test log")
208
209         for line in testlog:
210             if re.match('Test:.*FAIL$', line):
211                 passed = False
212             elif passed != False and re.match('Test:.*PASS$', line):
213                 passed = True
214         testlog.close()
215         server_ok = super(WebserverTest, self).passed()
216         return PassFailResult(passed and server_ok)
217
218
219 @tests.add_test
220 class HTTPerfTest(WebCommon):
221     '''httperf webserver performance benchmark'''
222     name = "httperf"
223
224     def setup(self, *args):
225         super(HTTPerfTest, self).setup(*args)
226         self.nruns = 0
227
228     def _runtest(self, target, nclients, nconns, rate):
229         self.nruns += 1
230         nrun = self.nruns
231         httperfs = []
232         try:
233             for nclient in range(nclients):
234                 user, host = siteconfig.site.get_load_generator()
235                 assert(nrun < 100 and nclient < 100)
236                 filename = 'httperf_run%02d_%02d.txt' % (nrun, nclient)
237                 logfile = open(os.path.join(self.testdir, filename), 'w')
238                 debug.verbose('spawning httperf on %s' % host)
239                 hp = HTTPerfClient(logfile, user, host, target, nconns, rate)
240                 httperfs.append(hp)
241
242             # loop collecting output from all of them
243             busy_httperfs = list(httperfs) # copy list
244             timeout = datetime.datetime.now() + HTTPERF_TIMEOUT
245             while busy_httperfs:
246                 (ready, _, _) = select_timeout(timeout, busy_httperfs)
247                 if not ready:
248                     raise TimeoutError('waiting for httperfs')
249                 for hp in ready:
250                     try:
251                         hp.read()
252                     except EOFError:
253                         busy_httperfs.remove(hp)
254         finally:
255             debug.log('cleaning up httperf test...')
256             for hp in httperfs:
257                 hp.cleanup()
258
259     def runtests(self, target):
260         nclients = siteconfig.get('HTTPERF_MAXCLIENTS')
261         firstrun = True
262         totalrate = HTTPERF_STARTRATE
263         while True:
264             if firstrun:
265                 firstrun = False
266             else:
267                 # sleep a moment to let things settle down between runs
268                 debug.verbose('sleeping between httperf runs')
269                 time.sleep(HTTPERF_SLEEPTIME)
270
271             # compute rate and total number of connections for each client
272             rate = totalrate / nclients
273             nconns = HTTPERF_DURATION * rate
274
275             debug.log('starting httperf: %d clients, %d conns, rate %d (%d per client)' %
276                       (nclients, nconns, totalrate, rate))
277             self._runtest(target, nclients, nconns, rate)
278
279             # decide whether to keep going...
280             results = self._process_run(self.nruns)
281             if not results.passed():
282                 debug.log('previous test failed, stopping')
283                 break
284             elif results.request_rate < (0.9 * results.connect_rate):
285                 debug.log('request rate below 90% of connect rate, stopping')
286                 break
287             elif results.reply_rate < (0.9 * results.request_rate):
288                 debug.log('reply rate below 90% of request rate, stopping')
289                 break
290             else:
291                 totalrate += HTTPERF_RATEINCREMENT
292                 continue
293
294     def _process_one(self, logfile):
295         ret = HTTPerfResults()
296         matches = 0
297
298         for line in logfile:
299             # Connection rate
300             m = re.match('Connection rate: (\d+\.\d+) conn/s', line)
301             if m:
302                 matches += 1
303                 ret.connect_rate = float(m.group(1))
304
305             # Request rate
306             m = re.match('Request rate: (\d+\.\d+) req/s', line)
307             if m:
308                 matches += 1
309                 ret.request_rate = float(m.group(1))
310
311             # Reply rate
312             m = re.search('Reply rate \[replies/s\]: min .* avg (\d+\.\d+)'
313                           ' max .* stddev .*', line)
314             if m:
315                 matches += 1
316                 ret.reply_rate = float(m.group(1))
317
318             # Bandwidth
319             m = re.match('Net I/O: .* KB/s \((\d+\.\d+)\*10\^6 bps\)', line)
320             if m:
321                 matches += 1
322                 ret.bandwidth = float(m.group(1))
323
324             # client-side errors
325             m = re.match('Errors: fd-unavail (\d+) addrunavail (\d+)'
326                          ' ftab-full (\d+) other (\d+)', line)
327             if m:
328                 matches += 1
329                 ret.fd_unavail = int(m.group(1))
330                 ret.addrunavail = int(m.group(2))
331                 ret.ftab_full = int(m.group(3))
332                 ret.other_err = int(m.group(4))
333
334             # server-side errors
335             m = re.match('Errors: total \d+ client-timo (\d+) socket-timo (\d+)'
336                          ' connrefused (\d+) connreset (\d+)', line)
337             if m:
338                 matches += 1
339                 ret.client_timo = int(m.group(1))
340                 ret.socket_timo = int(m.group(2))
341                 ret.connrefused = int(m.group(3))
342                 ret.connreset = int(m.group(4))
343
344         if matches != 6 : # otherwise we have an invalid log
345             print "Instead of 6, only %d matches found\n" % (matches)
346
347         return ret
348
349
350     def _process_run(self, nrun):
351         nameglob = 'httperf_run%02d_*.txt' % nrun
352         results = []
353         for filename in glob.iglob(os.path.join(self.testdir, nameglob)):
354             with open(filename, 'r') as logfile:
355                 results.append(self._process_one(logfile))
356         return sum(results, HTTPerfResults())
357
358     def process_data(self, testdir, raw_iter):
359         self.testdir = testdir
360         totals = {}
361         for filename in glob.iglob(os.path.join(testdir, 'httperf_run*.txt')):
362             nrun = int(re.match('.*/httperf_run(\d+)_', filename).group(1))
363             result = self._process_run(nrun)
364             totals[nrun] = result
365
366         fields = 'run connect_rate request_rate reply_rate bandwidth errors'.split()
367         final = RowResults(fields)
368
369         for run in sorted(totals.keys()):
370             total = totals[run]
371             errsum = sum([getattr(total, f) for f in total._err_fields])
372             final.add_row([run, total.connect_rate, total.request_rate,
373                            total.reply_rate, total.bandwidth, errsum])
374             # XXX: often the last run will have errors in it, due to the control algorithm
375             #if errsum:
376             #    final.mark_failed()
377
378         # If we saw a severe failure (assertion failure, kernel panic, or user
379         # level panic) in the webserver, fail the test
380         if not super(HTTPerfTest, self).passed():
381             final.mark_failed('\n'.join(self.server_failures))
382
383         return final
384
385
386 class HTTPerfResults(ResultsBase):
387     _err_fields = 'fd_unavail addrunavail ftab_full other_err'.split()
388     _result_fields = ('client_timo socket_timo connrefused connreset'
389                       ' connect_rate request_rate bandwidth reply_rate').split()
390     _fields = _err_fields + _result_fields
391
392     def __init__(self):
393         super(HTTPerfResults, self).__init__()
394         for f in self._fields:
395             setattr(self, f, 0)
396
397     def __add__(self, other):
398         ret = HTTPerfResults()
399         for f in self._fields:
400             setattr(ret, f, getattr(self, f) + getattr(other, f))
401         return ret
402
403     def passed(self):
404         return all([getattr(self, field) == 0 for field in self._err_fields])
405
406     def to_file(self, fh):
407         errs = [(f, getattr(self,f)) for f in self._err_fields if getattr(self,f)]
408         if errs:
409             fh.write('Failed run: ' + ' '.join(['%s %d' % e for e in errs]))
410
411         fh.write('Request rate:\t%f\n' % self.request_rate)
412         fh.write('Bandwidth:\t%f\n' % self.bandwidth)
413         fh.write('Reply rate:\t%f\n' % self.reply_rate)
414
415
416 class HTTPerfClient(object):
417     def __init__(self, logfile, user, host, target, nconns, rate):
418         self.user = user
419         self.host = host
420         self.httperf_path = siteconfig.get('HTTPERF_PATH')
421         cmd = '%s %s' % (self.httperf_path, HTTPERF_BASE_ARGS)
422         cmd += ' --num-conns %d --rate %d --server %s --uri %s' % (
423                         nconns, rate, target, HTTPERF_URI)
424         self.proc = self._launchssh(cmd, stdout=subprocess.PIPE, bufsize=0)
425         self.logfile = logfile
426
427     def _launchssh(self, remotecmd, **kwargs):
428         ssh_dest = '%s@%s' % (self.user, self.host)
429         cmd = ['ssh'] + siteconfig.get('SSH_ARGS').split() + [ssh_dest, remotecmd]
430         return subprocess.Popen(cmd, **kwargs)
431
432     # mirror builtin file method so that we can pass this to select()
433     def fileno(self):
434         return self.proc.stdout.fileno()
435
436     def read(self):
437         # read only a single character to avoid blocking!
438         s = self.proc.stdout.read(1)
439         if s == '':
440             raise EOFError
441         self.logfile.write(s)
442
443     def cleanup(self):
444         """perform cleanup if necessary"""
445         self.logfile.close()
446         if self.proc is None or self.proc.poll() == 0:
447             return # clean exit
448
449         if self.proc.returncode:
450             debug.warning('httperf: SSH to %s exited with error %d'
451                           % (self.host, self.proc.returncode))
452         else: # kill SSH if still up
453             debug.warning('httperf: killing SSH child for %s' % self.host)
454             self.proc.terminate()
455             self.proc.wait()
456
457         # run a remote killall to get rid of any errant httperfs
458         debug.verbose('killing any errant httperfs on %s' % self.host)
459         p = self._launchssh('killall -q %s' % self.httperf_path)
460         retcode = p.wait()
461         if retcode != 0:
462             debug.warning('failed to killall httperf on %s!' % self.host)