blob: 556a389285a860eb48404af46c779c1393fcab18 [file] [log] [blame]
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import time
import os.path
from autotest_lib.client.common_lib import error
class NetperfResult(object):
"""Encapsulates logic to parse and represent netperf results."""
@staticmethod
def from_netperf_results(test_type, results, duration_seconds):
"""Parse the text output of netperf and return a NetperfResult.
@param test_type string one of NetperfConfig.TEST_TYPE_* below.
@param results string raw results from netperf.
@param duration_seconds float number of seconds the test ran for.
@return NetperfResult result.
"""
lines = results.splitlines()
if test_type in NetperfConfig.TCP_STREAM_TESTS:
"""Parses the following (works for both TCP_STREAM, TCP_MAERTS and
TCP_SENDFILE) and returns a singleton containing throughput.
TCP STREAM TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to \
foo.bar.com (10.10.10.3) port 0 AF_INET
Recv Send Send
Socket Socket Message Elapsed
Size Size Size Time Throughput
bytes bytes bytes secs. 10^6bits/sec
87380 16384 16384 2.00 941.28
"""
return NetperfResult(duration_seconds,
throughput=float(lines[6].split()[4]))
if test_type in NetperfConfig.UDP_STREAM_TESTS:
"""Parses the following and returns a tuple containing throughput
and the number of errors.
UDP UNIDIRECTIONAL SEND TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET \
to foo.bar.com (10.10.10.3) port 0 AF_INET
Socket Message Elapsed Messages
Size Size Time Okay Errors Throughput
bytes bytes secs # # 10^6bits/sec
129024 65507 2.00 3673 0 961.87
131072 2.00 3673 961.87
"""
udp_tokens = lines[5].split()
return NetperfResult(duration_seconds,
throughput=float(udp_tokens[5]),
errors=float(udp_tokens[4]))
if test_type in NetperfConfig.REQUEST_RESPONSE_TESTS:
"""Parses the following which works for both rr (TCP and UDP)
and crr tests and returns a singleton containing transfer rate.
TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET \
to foo.bar.com (10.10.10.3) port 0 AF_INET
Local /Remote
Socket Size Request Resp. Elapsed Trans.
Send Recv Size Size Time Rate
bytes Bytes bytes bytes secs. per sec
16384 87380 1 1 2.00 14118.53
16384 87380
"""
return NetperfResult(duration_seconds,
transaction_rate=float(lines[6].split()[5]))
raise error.TestFail('Invalid netperf test type: %r.' % test_type)
def __init__(self, duration_seconds, throughput=None,
errors=None, transaction_rate=None):
"""Construct a NetperfResult.
@param duration_seconds float how long the test took.
@param throughput float test throughput in Mbps.
@param errors int number of UDP errors in test.
@param transaction_rate float transactions per second.
"""
self.duration_seconds = duration_seconds
self.throughput = throughput
self.errors = errors
self.transaction_rate = transaction_rate
logging.info('netperf duration: %f seconds', duration_seconds)
if throughput is not None:
logging.info('netperf throughput: %f Mbps', throughput)
if errors is not None:
logging.info('netperf errors: %f UDP errors', errors)
if transaction_rate is not None:
logging.info('netperf transaction_rate: %f transactions/sec',
transaction_rate)
def __repr__(self):
return ('%s(duration_seconds=%f, throughput=%r, '
'errors=%r, transaction_rate=%r)' % (self.__class__.__name__,
self.duration_seconds,
self.throughput,
self.errors,
self.transaction_rate))
class NetperfAssertion(object):
"""Defines a set of expectations for netperf results."""
def __init__(self, duration_min=None, duration_max=None,
throughput_min=None, throughput_max=None,
error_min=None, error_max=None,
transaction_rate_min=None, transaction_rate_max=None):
"""Construct a NetperfAssertion.
Leaving bounds undefined sets them to values which are permissive.
@param duration_min float minimal test duration in seconds.
@param duration_max float maximal test duration in seconds.
@param throughput_min float minimal throughput in Mbps.
@param throughput_max float maximal throughput in Mbps.
@param error_min int minimal number of UDP frame errors.
@param error_max int max number of UDP frame errors.
@param transaction_rate_min float minimal number of transactions
per second.
@param transaction_rate_max float max number of transactions per second.
"""
self.duration_bounds = (duration_min, duration_max)
self.throughput_bounds = (throughput_min, throughput_max)
self.error_bounds = (error_min, error_max)
self.transaction_rate_bounds = (transaction_rate_min,
transaction_rate_max)
def _passes(self, value, bounds):
if bounds[0] is None and bounds[1] is None:
return True
if value is None:
# We have bounds requirements, but no value to check?
return False
if bounds[0] is not None and bounds[0] > value:
return False
if bounds[1] is not None and bounds[1] < value:
return False
return True
def passes(self, result):
"""Check that a result matches the given assertion.
@param result NetperfResult object produced by a test.
@return True iff all this assertion passes for the give result.
"""
if (self._passes(result.duration_seconds, self.duration_bounds) and
self._passes(result.throughput, self.throughput_bounds) and
self._passes(result.errors, self.error_bounds) and
self._passes(result.transaction_rate,
self.transaction_rate_bounds)):
return True
return False
def __repr__(self):
return ('%s(duration_min=%r, duration_max=%r, '
'thoughput_min=%r, throughput_max=%r, '
'error_min=%r, error_max=%r, '
'transaction_rate_min=%r, transaction_rate_max=%r)' % (
self.__class__.__name__,
self.duration_bounds[0], self.duration_bounds[1],
self.throughput_bounds[0], self.throughput_bounds[1],
self.error_bounds[0], self.error_bounds[1],
self.transaction_rate_bounds[0],
self.transaction_rate_bounds[1]))
class NetperfConfig(object):
"""Defines a single netperf run."""
DEFAULT_TEST_TIME = 15
# Measures how many times we can connect, request a byte, and receive a
# byte per second.
TEST_TYPE_TCP_CRR = 'TCP_CRR'
# MAERTS is stream backwards. Measure bitrate of a stream from the netperf
# server to the client.
TEST_TYPE_TCP_MAERTS = 'TCP_MAERTS'
# Measures how many times we can request a byte and receive a byte per
# second.
TEST_TYPE_TCP_RR = 'TCP_RR'
# This is like a TCP_STREAM test except that the netperf client will use
# a platform dependent call like sendfile() rather than the simple send()
# call. This can result in better performance.
TEST_TYPE_TCP_SENDFILE = 'TCP_SENDFILE'
# Measures throughput sending bytes from the client to the server in a
# TCP stream.
TEST_TYPE_TCP_STREAM = 'TCP_STREAM'
# Measures how many times we can request a byte from the client and receive
# a byte from the server. If any datagram is dropped, the client or server
# will block indefinitely. This failure is not evident except as a low
# transaction rate.
TEST_TYPE_UDP_RR = 'UDP_RR'
# Test UDP throughput sending from the client to the server. There is no
# flow control here, and generally sending is easier that receiving, so
# there will be two types of throughput, both receiving and sending.
TEST_TYPE_UDP_STREAM = 'UDP_STREAM'
# Different kinds of tests have different output formats.
REQUEST_RESPONSE_TESTS = [ TEST_TYPE_TCP_CRR,
TEST_TYPE_TCP_RR,
TEST_TYPE_UDP_RR ]
TCP_STREAM_TESTS = [ TEST_TYPE_TCP_MAERTS,
TEST_TYPE_TCP_SENDFILE,
TEST_TYPE_TCP_STREAM ]
UDP_STREAM_TESTS = [ TEST_TYPE_UDP_STREAM ]
@staticmethod
def _assert_is_valid_test_type(test_type):
"""Assert that |test_type| is one of TEST_TYPE_* above.
@param test_type string test type.
"""
if (test_type not in NetperfConfig.REQUEST_RESPONSE_TESTS and
test_type not in NetperfConfig.TCP_STREAM_TESTS and
test_type not in NetperfConfig.UDP_STREAM_TESTS):
raise error.TestFail('Invalid netperf test type: %r.' % test_type)
def __init__(self, test_type, server_serves=True, test_time=None):
"""Construct a NetperfConfig.
@param test_type string one of TEST_TYPE_* above.
@param server_serves bool True iff server is acting as the netperf
server.
@param test_time int number of seconds to run the test for.
"""
self._assert_is_valid_test_type(test_type)
self.test_type = test_type
self.server_serves = server_serves
self.test_time = test_time or self.DEFAULT_TEST_TIME
def __repr__(self):
return '%s(test_type=%r, server_serves=%r, test_time=%r' % (
self.__class__.__name__,
self.test_type,
self.server_serves,
self.test_time)
class NetperfRunner(object):
"""Delegate to run netperf on a client/server pair."""
NETPERF_DATA_PORT = 12866
NETPERF_PORT = 12865
NETSERV_STARTUP_WAIT_TIME = 3
NETPERF_COMMAND_TIMEOUT_MARGIN = 5
def __init__(self, client, server):
"""Construct a NetperfRunner.
@param client WiFiClient object.
@param server LinuxServer object.
"""
self._client_proxy = client
self._server_proxy = server
def _run_body(self, client_host, server_host, netperf, netserv, timeout):
"""Actually run the commands on the remote hosts.
This method contains all the calls for a netperf run that are likely to
fail. As such, it should only be called in a context where we know that
suitable cleanup will happen in case of a failure.
@param client_host Host object representing the 'client' test role.
@param server_host Host object representing the 'server' test role.
@param netperf string complete command with args to start netperf.
@param netserv string complete command with args to start netserv.
@param timeout int number of seconds to give the netperf command.
@return tuple (raw netperf output, duration of run in seconds).
"""
logging.info('Starting netserver with command %s.', netserv)
server_host.run(netserv)
# Wait for the netserv to come up.
time.sleep(self.NETSERV_STARTUP_WAIT_TIME)
logging.info('Running netperf client with command %s.', netperf)
start_time = time.time()
result = client_host.run(netperf, timeout=timeout)
duration = time.time() - start_time
return result.stdout, duration
def run(self, config):
"""Run netperf.
@param config NetperfConfig defines the parameters of the test.
@return NetperfResult summarizing the resulting test.
"""
if config.server_serves:
server_host = self._server_proxy.host
client_host = self._client_proxy.host
command_netserv = self._server_proxy.cmd_netserv
command_netperf = self._client_proxy.command_netperf
target_ip = self._server_proxy.wifi_ip
else:
server_host = self._client_proxy.host
client_host = self._server_proxy.host
command_netserv = self._client_proxy.command_netserv
command_netperf = self._server_proxy.cmd_netperf
target_ip = self._client_proxy.wifi_ip
netserv = '%s -p %d &> /dev/null' % (command_netserv, self.NETPERF_PORT)
netperf = '%s -H %s -p %s -t %s -l %d -- -P 0,%d' % (
command_netperf,
target_ip,
self.NETPERF_PORT,
config.test_type,
config.test_time,
self.NETPERF_DATA_PORT)
self._client_proxy.firewall_open('tcp', self._server_proxy.wifi_ip)
self._client_proxy.firewall_open('udp', self._server_proxy.wifi_ip)
try:
raw_result,duration = self._run_body(
client_host, server_host, netperf, netserv,
config.test_time + self.NETPERF_COMMAND_TIMEOUT_MARGIN)
finally:
server_host.run('pkill %s' % os.path.basename(command_netserv))
self._client_proxy.firewall_cleanup()
return NetperfResult.from_netperf_results(config.test_type, raw_result,
duration)