1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import collections 6import logging 7import math 8import numbers 9import re 10import time 11import os.path 12 13from autotest_lib.client.common_lib import error 14from autotest_lib.client.common_lib.cros import path_utils 15 16 17class NetperfResult(object): 18 """Encapsulates logic to parse and represent netperf results.""" 19 20 @staticmethod 21 def from_netperf_results(test_type, results, duration_seconds): 22 """Parse the text output of netperf and return a NetperfResult. 23 24 @param test_type string one of NetperfConfig.TEST_TYPE_* below. 25 @param results string raw results from netperf. 26 @param duration_seconds float number of seconds the test ran for. 27 @return NetperfResult result. 28 29 """ 30 lines = results.splitlines() 31 32 # Include only results lines, which should start with a number. This 33 # helps eliminate inconsistent output, e.g., from benign warnings 34 # like: 35 # catcher: timer popped with times_up != 0 36 lines = [l for l in lines if re.match('[0-9]+', l.strip())] 37 38 if test_type in NetperfConfig.TCP_STREAM_TESTS: 39 """Parses the following (works for both TCP_STREAM, TCP_MAERTS and 40 TCP_SENDFILE) and returns a singleton containing throughput. 41 42 TCP STREAM TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to \ 43 foo.bar.com (10.10.10.3) port 0 AF_INET 44 Recv Send Send 45 Socket Socket Message Elapsed 46 Size Size Size Time Throughput 47 bytes bytes bytes secs. 10^6bits/sec 48 49 87380 16384 16384 2.00 941.28 50 """ 51 if len(lines) < 1: 52 return None 53 54 result = NetperfResult(test_type, duration_seconds, 55 throughput=float(lines[0].split()[4])) 56 elif test_type in NetperfConfig.UDP_STREAM_TESTS: 57 """Parses the following and returns a tuple containing throughput 58 and the number of errors. 59 60 UDP UNIDIRECTIONAL SEND TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET \ 61 to foo.bar.com (10.10.10.3) port 0 AF_INET 62 Socket Message Elapsed Messages 63 Size Size Time Okay Errors Throughput 64 bytes bytes secs # # 10^6bits/sec 65 66 129024 65507 2.00 3673 0 961.87 67 131072 2.00 3673 961.87 68 """ 69 if len(lines) < 1: 70 return None 71 72 udp_tokens = lines[0].split() 73 result = NetperfResult(test_type, duration_seconds, 74 throughput=float(udp_tokens[5]), 75 errors=float(udp_tokens[4])) 76 elif test_type in NetperfConfig.REQUEST_RESPONSE_TESTS: 77 """Parses the following which works for both rr (TCP and UDP) 78 and crr tests and returns a singleton containing transfer rate. 79 80 TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET \ 81 to foo.bar.com (10.10.10.3) port 0 AF_INET 82 Local /Remote 83 Socket Size Request Resp. Elapsed Trans. 84 Send Recv Size Size Time Rate 85 bytes Bytes bytes bytes secs. per sec 86 87 16384 87380 1 1 2.00 14118.53 88 16384 87380 89 """ 90 if len(lines) < 1: 91 return None 92 93 result = NetperfResult(test_type, duration_seconds, 94 transaction_rate=float(lines[0].split()[5])) 95 else: 96 raise error.TestFail('Invalid netperf test type: %r.' % test_type) 97 98 logging.info('%r', result) 99 return result 100 101 102 @staticmethod 103 def _get_stats(samples, field_name): 104 if any(map(lambda x: getattr(x, field_name) is None, samples)): 105 return (None, None) 106 107 values = map(lambda x: getattr(x, field_name), samples) 108 N = len(samples) 109 mean = math.fsum(values) / N 110 deviation = None 111 if N > 1: 112 differences = map(lambda x: math.pow(mean - x, 2), values) 113 deviation = math.sqrt(math.fsum(differences) / (N - 1)) 114 return mean, deviation 115 116 117 @staticmethod 118 def from_samples(samples): 119 """Build an averaged NetperfResult from |samples|. 120 121 Calculate an representative sample with averaged values 122 and standard deviation from samples. 123 124 @param samples list of NetperfResult objects. 125 @return NetperfResult object. 126 127 """ 128 if len(set([x.test_type for x in samples])) != 1: 129 # We have either no samples or multiple test types. 130 return None 131 132 duration_seconds, duration_seconds_dev = NetperfResult._get_stats( 133 samples, 'duration_seconds') 134 throughput, throughput_dev = NetperfResult._get_stats( 135 samples, 'throughput') 136 errors, errors_dev = NetperfResult._get_stats(samples, 'errors') 137 transaction_rate, transaction_rate_dev = NetperfResult._get_stats( 138 samples, 'transaction_rate') 139 return NetperfResult( 140 samples[0].test_type, 141 duration_seconds, duration_seconds_dev=duration_seconds_dev, 142 throughput=throughput, throughput_dev=throughput_dev, 143 errors=errors, errors_dev=errors_dev, 144 transaction_rate=transaction_rate, 145 transaction_rate_dev=transaction_rate_dev) 146 147 148 @property 149 def human_readable_tag(self): 150 """@return string human readable test description.""" 151 return NetperfConfig.test_type_to_human_readable_tag(self.test_type) 152 153 154 @property 155 def tag(self): 156 """@return string very short test description.""" 157 return NetperfConfig.test_type_to_tag(self.test_type) 158 159 160 def __init__(self, test_type, duration_seconds, duration_seconds_dev=None, 161 throughput=None, throughput_dev=None, 162 errors=None, errors_dev=None, 163 transaction_rate=None, transaction_rate_dev=None): 164 """Construct a NetperfResult. 165 166 @param duration_seconds float how long the test took. 167 @param throughput float test throughput in Mbps. 168 @param errors int number of UDP errors in test. 169 @param transaction_rate float transactions per second. 170 171 """ 172 self.test_type = test_type 173 self.duration_seconds = duration_seconds 174 self.duration_seconds_dev = duration_seconds_dev 175 self.throughput = throughput 176 self.throughput_dev = throughput_dev 177 self.errors = errors 178 self.errors_dev = errors_dev 179 self.transaction_rate = transaction_rate 180 self.transaction_rate_dev = transaction_rate_dev 181 if throughput is None and transaction_rate is None and errors is None: 182 logging.error('Created a NetperfResult with no data.') 183 184 185 def __repr__(self): 186 fields = ['test_type=%s' % self.test_type] 187 fields += ['%s=%0.2f' % item 188 for item in vars(self).iteritems() 189 if item[1] is not None 190 and isinstance(item[1], numbers.Number)] 191 return '%s(%s)' % (self.__class__.__name__, ', '.join(fields)) 192 193 194 def all_deviations_less_than_fraction(self, fraction): 195 """Check that this result is "acurate" enough. 196 197 We say that a NetperfResult is "acurate" enough when for each 198 measurement X with standard deviation d(X), d(X)/X <= |fraction|. 199 200 @param fraction float used in constraint above. 201 @return True on above condition. 202 203 """ 204 for measurement in ['throughput', 'errors', 'transaction_rate']: 205 value = getattr(self, measurement) 206 dev = getattr(self, measurement + '_dev') 207 if value is None or dev is None: 208 continue 209 210 if not dev and not value: 211 # 0/0 is undefined, but take this to be good for our purposes. 212 continue 213 214 if dev and not value: 215 # Deviation is non-zero, but the average is 0. Deviation 216 # as a fraction of the value is undefined but in theory 217 # a "very large number." 218 return False 219 220 if dev / value > fraction: 221 return False 222 223 return True 224 225 226 def get_keyval(self, prefix='', suffix=''): 227 ret = {} 228 if prefix: 229 prefix = prefix + '_' 230 if suffix: 231 suffix = '_' + suffix 232 233 for measurement in ['throughput', 'errors', 'transaction_rate']: 234 value = getattr(self, measurement) 235 dev = getattr(self, measurement + '_dev') 236 if dev is None: 237 margin = '' 238 else: 239 margin = '+-%0.2f' % dev 240 if value is not None: 241 ret[prefix + measurement + suffix] = '%0.2f%s' % (value, margin) 242 return ret 243 244 245class NetperfAssertion(object): 246 """Defines a set of expectations for netperf results.""" 247 248 def _passes(self, result, field): 249 value = getattr(result, field) 250 deviation = getattr(result, field + '_dev') 251 bounds = getattr(self, field + '_bounds') 252 if bounds[0] is None and bounds[1] is None: 253 return True 254 255 if value is None: 256 # We have bounds requirements, but no value to check? 257 return False 258 259 if bounds[0] is not None and bounds[0] > value + deviation: 260 return False 261 262 if bounds[1] is not None and bounds[1] < value - deviation: 263 return False 264 265 return True 266 267 268 def __init__(self, duration_seconds_min=None, duration_seconds_max=None, 269 throughput_min=None, throughput_max=None, 270 error_min=None, error_max=None, 271 transaction_rate_min=None, transaction_rate_max=None): 272 """Construct a NetperfAssertion. 273 274 Leaving bounds undefined sets them to values which are permissive. 275 276 @param duration_seconds_min float minimal test duration in seconds. 277 @param duration_seconds_max float maximal test duration in seconds. 278 @param throughput_min float minimal throughput in Mbps. 279 @param throughput_max float maximal throughput in Mbps. 280 @param error_min int minimal number of UDP frame errors. 281 @param error_max int max number of UDP frame errors. 282 @param transaction_rate_min float minimal number of transactions 283 per second. 284 @param transaction_rate_max float max number of transactions per second. 285 286 """ 287 Bound = collections.namedtuple('Bound', ['lower', 'upper']) 288 self.duration_seconds_bounds = Bound(duration_seconds_min, 289 duration_seconds_max) 290 self.throughput_bounds = Bound(throughput_min, throughput_max) 291 self.errors_bounds = Bound(error_min, error_max) 292 self.transaction_rate_bounds = Bound(transaction_rate_min, 293 transaction_rate_max) 294 295 296 def passes(self, result): 297 """Check that a result matches the given assertion. 298 299 @param result NetperfResult object produced by a test. 300 @return True iff all this assertion passes for the give result. 301 302 """ 303 passed = [self._passes(result, field) 304 for field in ['duration_seconds', 'throughput', 305 'errors', 'transaction_rate']] 306 if all(passed): 307 return True 308 309 return False 310 311 312 def __repr__(self): 313 fields = {'duration_seconds_min': self.duration_seconds_bounds.lower, 314 'duration_seconds_max': self.duration_seconds_bounds.upper, 315 'throughput_min': self.throughput_bounds.lower, 316 'throughput_max': self.throughput_bounds.upper, 317 'error_min': self.errors_bounds.lower, 318 'error_max': self.errors_bounds.upper, 319 'transaction_rate_min': self.transaction_rate_bounds.lower, 320 'transaction_rate_max': self.transaction_rate_bounds.upper} 321 return '%s(%s)' % (self.__class__.__name__, 322 ', '.join(['%s=%r' % item 323 for item in fields.iteritems() 324 if item[1] is not None])) 325 326 327class NetperfConfig(object): 328 """Defines a single netperf run.""" 329 330 DEFAULT_TEST_TIME = 10 331 # Measures how many times we can connect, request a byte, and receive a 332 # byte per second. 333 TEST_TYPE_TCP_CRR = 'TCP_CRR' 334 # MAERTS is stream backwards. Measure bitrate of a stream from the netperf 335 # server to the client. 336 TEST_TYPE_TCP_MAERTS = 'TCP_MAERTS' 337 # Measures how many times we can request a byte and receive a byte per 338 # second. 339 TEST_TYPE_TCP_RR = 'TCP_RR' 340 # This is like a TCP_STREAM test except that the netperf client will use 341 # a platform dependent call like sendfile() rather than the simple send() 342 # call. This can result in better performance. 343 TEST_TYPE_TCP_SENDFILE = 'TCP_SENDFILE' 344 # Measures throughput sending bytes from the client to the server in a 345 # TCP stream. 346 TEST_TYPE_TCP_STREAM = 'TCP_STREAM' 347 # Measures how many times we can request a byte from the client and receive 348 # a byte from the server. If any datagram is dropped, the client or server 349 # will block indefinitely. This failure is not evident except as a low 350 # transaction rate. 351 TEST_TYPE_UDP_RR = 'UDP_RR' 352 # Test UDP throughput sending from the client to the server. There is no 353 # flow control here, and generally sending is easier that receiving, so 354 # there will be two types of throughput, both receiving and sending. 355 TEST_TYPE_UDP_STREAM = 'UDP_STREAM' 356 # This isn't a real test type, but we can emulate a UDP stream from the 357 # server to the DUT by running the netperf server on the DUT and the 358 # client on the server and then doing a UDP_STREAM test. 359 TEST_TYPE_UDP_MAERTS = 'UDP_MAERTS' 360 # Different kinds of tests have different output formats. 361 REQUEST_RESPONSE_TESTS = [ TEST_TYPE_TCP_CRR, 362 TEST_TYPE_TCP_RR, 363 TEST_TYPE_UDP_RR ] 364 TCP_STREAM_TESTS = [ TEST_TYPE_TCP_MAERTS, 365 TEST_TYPE_TCP_SENDFILE, 366 TEST_TYPE_TCP_STREAM ] 367 UDP_STREAM_TESTS = [ TEST_TYPE_UDP_STREAM, 368 TEST_TYPE_UDP_MAERTS ] 369 370 SHORT_TAGS = { TEST_TYPE_TCP_CRR: 'tcp_crr', 371 TEST_TYPE_TCP_MAERTS: 'tcp_rx', 372 TEST_TYPE_TCP_RR: 'tcp_rr', 373 TEST_TYPE_TCP_SENDFILE: 'tcp_stx', 374 TEST_TYPE_TCP_STREAM: 'tcp_tx', 375 TEST_TYPE_UDP_RR: 'udp_rr', 376 TEST_TYPE_UDP_STREAM: 'udp_tx', 377 TEST_TYPE_UDP_MAERTS: 'udp_rx' } 378 379 READABLE_TAGS = { TEST_TYPE_TCP_CRR: 'tcp_connect_roundtrip_rate', 380 TEST_TYPE_TCP_MAERTS: 'tcp_downstream', 381 TEST_TYPE_TCP_RR: 'tcp_roundtrip_rate', 382 TEST_TYPE_TCP_SENDFILE: 'tcp_upstream_sendfile', 383 TEST_TYPE_TCP_STREAM: 'tcp_upstream', 384 TEST_TYPE_UDP_RR: 'udp_roundtrip', 385 TEST_TYPE_UDP_STREAM: 'udp_upstream', 386 TEST_TYPE_UDP_MAERTS: 'udp_downstream' } 387 388 389 @staticmethod 390 def _assert_is_valid_test_type(test_type): 391 """Assert that |test_type| is one of TEST_TYPE_* above. 392 393 @param test_type string test type. 394 395 """ 396 if (test_type not in NetperfConfig.REQUEST_RESPONSE_TESTS and 397 test_type not in NetperfConfig.TCP_STREAM_TESTS and 398 test_type not in NetperfConfig.UDP_STREAM_TESTS): 399 raise error.TestFail('Invalid netperf test type: %r.' % test_type) 400 401 402 @staticmethod 403 def test_type_to_tag(test_type): 404 """Convert a test type to a concise unique tag. 405 406 @param test_type string, one of TEST_TYPE_* above. 407 @return string very short test description. 408 409 """ 410 return NetperfConfig.SHORT_TAGS.get(test_type, 'unknown') 411 412 413 @staticmethod 414 def test_type_to_human_readable_tag(test_type): 415 """Convert a test type to a unique human readable tag. 416 417 @param test_type string, one of TEST_TYPE_* above. 418 @return string human readable test description. 419 420 """ 421 return NetperfConfig.READABLE_TAGS.get(test_type, 'unknown') 422 423 @property 424 def human_readable_tag(self): 425 """@return string human readable test description.""" 426 return self.test_type_to_human_readable_tag(self.test_type) 427 428 429 @property 430 def netperf_test_type(self): 431 """@return string test type suitable for passing to netperf.""" 432 if self.test_type == self.TEST_TYPE_UDP_MAERTS: 433 return self.TEST_TYPE_UDP_STREAM 434 435 return self.test_type 436 437 438 @property 439 def server_serves(self): 440 """False iff the server and DUT should switch roles for running netperf. 441 442 @return True iff netserv should be run on server host. When false 443 this indicates that the DUT should run netserv and netperf 444 should be run on the server against the client. 445 446 """ 447 return self.test_type != self.TEST_TYPE_UDP_MAERTS 448 449 450 @property 451 def tag(self): 452 """@return string very short test description.""" 453 return self.test_type_to_tag(self.test_type) 454 455 456 def __init__(self, test_type, test_time=DEFAULT_TEST_TIME): 457 """Construct a NetperfConfig. 458 459 @param test_type string one of TEST_TYPE_* above. 460 @param test_time int number of seconds to run the test for. 461 462 """ 463 self.test_type = test_type 464 self.test_time = test_time 465 self._assert_is_valid_test_type(self.netperf_test_type) 466 467 468 def __repr__(self): 469 return '%s(test_type=%r, test_time=%r' % ( 470 self.__class__.__name__, 471 self.test_type, 472 self.test_time) 473 474 475class NetperfRunner(object): 476 """Delegate to run netperf on a client/server pair.""" 477 478 NETPERF_DATA_PORT = 12866 479 NETPERF_PORT = 12865 480 NETSERV_STARTUP_WAIT_TIME = 3 481 NETPERF_COMMAND_TIMEOUT_MARGIN = 60 482 483 484 def __init__(self, client_proxy, server_proxy, config): 485 """Construct a NetperfRunner. 486 487 @param client WiFiClient object. 488 @param server LinuxSystem object. 489 490 """ 491 self._client_proxy = client_proxy 492 self._server_proxy = server_proxy 493 if config.server_serves: 494 self._server_host = server_proxy.host 495 self._client_host = client_proxy.host 496 self._target_ip = server_proxy.wifi_ip 497 else: 498 self._server_host = client_proxy.host 499 self._client_host = server_proxy.host 500 self._target_ip = client_proxy.wifi_ip 501 502 # Assume minijail0 is on ${PATH}, but raise exception if it's not 503 # available on both server and client. 504 self._minijail = 'minijail0' 505 path_utils.must_be_installed(self._minijail, host=self._server_host) 506 path_utils.must_be_installed(self._minijail, host=self._client_host) 507 # Bind mount a tmpfs over /tmp, since netserver hard-codes the /tmp 508 # path. netserver's log files aren't useful anyway. 509 self._minijail = ("%s -v -k 'tmpfs,/tmp,tmpfs," 510 "MS_NODEV|MS_NOEXEC|MS_NOSUID,mode=755,size=10M'" 511 % self._minijail) 512 513 self._command_netserv = path_utils.must_be_installed( 514 'netserver', host=self._server_host) 515 self._command_netperf = path_utils.must_be_installed( 516 'netperf', host=self._client_host) 517 self._config = config 518 519 520 def __enter__(self): 521 self._restart_netserv() 522 return self 523 524 525 def __exit__(self, exc_type, exc_value, traceback): 526 self._client_proxy.firewall_cleanup() 527 self._kill_netserv() 528 529 530 def _kill_netserv(self): 531 """Kills any existing netserv process on the serving host.""" 532 self._server_host.run('pkill %s' % 533 os.path.basename(self._command_netserv), 534 ignore_status=True) 535 536 537 def _restart_netserv(self): 538 logging.info('Starting netserver...') 539 self._kill_netserv() 540 self._server_host.run('%s %s -p %d' % 541 (self._minijail, self._command_netserv, 542 self.NETPERF_PORT)) 543 startup_time = time.time() 544 self._client_proxy.firewall_open('tcp', self._server_proxy.wifi_ip) 545 self._client_proxy.firewall_open('udp', self._server_proxy.wifi_ip) 546 # Wait for the netserv to come up. 547 while time.time() - startup_time < self.NETSERV_STARTUP_WAIT_TIME: 548 time.sleep(0.1) 549 550 551 def run(self, ignore_failures=False, retry_count=3): 552 """Run netperf and take a performance measurement. 553 554 @param ignore_failures bool True iff netperf runs that fail should be 555 ignored. If this happens, run will return a None value rather 556 than a NetperfResult. 557 @param retry_count int number of times to retry the netperf command if 558 it fails due to an internal timeout within netperf. 559 @return NetperfResult summarizing a netperf run. 560 561 """ 562 netperf = '%s -H %s -p %s -t %s -l %d -- -P 0,%d' % ( 563 self._command_netperf, 564 self._target_ip, 565 self.NETPERF_PORT, 566 self._config.netperf_test_type, 567 self._config.test_time, 568 self.NETPERF_DATA_PORT) 569 logging.debug('Running netperf client.') 570 logging.info('Running netperf for %d seconds.', self._config.test_time) 571 timeout = self._config.test_time + self.NETPERF_COMMAND_TIMEOUT_MARGIN 572 for _ in range(retry_count): 573 start_time = time.time() 574 result = self._client_host.run(netperf, ignore_status=True, 575 ignore_timeout=ignore_failures, 576 timeout=timeout) 577 if not result: 578 logging.info('Retrying netperf after empty result.') 579 continue 580 581 # Exit retry loop on success. 582 if not result.exit_status: 583 break 584 585 # Only retry for known retryable conditions. 586 if 'Interrupted system call' in result.stderr: 587 logging.info('Retrying netperf after internal timeout error.') 588 continue 589 590 if 'establish the control connection' in result.stdout: 591 logging.info('Restarting netserv after client failed connect.') 592 self._restart_netserv() 593 continue 594 595 # We are in an unhandled error case. 596 logging.info('Retrying netperf after an unknown error.') 597 598 if ignore_failures and (result is None or result.exit_status): 599 return None 600 601 if result is None: 602 raise error.TestFail("No results; cmd: %s", netperf) 603 604 if result.exit_status: 605 raise error.CmdError(netperf, result, 606 "Command returned non-zero exit status") 607 608 duration = time.time() - start_time 609 return NetperfResult.from_netperf_results( 610 self._config.test_type, result.stdout, duration) 611