blob: 9b952c55550264e2ef14415c2dc35364da7ddfb1 [file] [log] [blame]
#!/usr/bin/env python3.4
#
# Copyright 2016 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import math
import os
from acts import utils
from acts.controllers import android_device
from acts.controllers.utils_lib.ssh import connection
from acts.controllers.utils_lib.ssh import settings
ACTS_CONTROLLER_CONFIG_NAME = "IPerfServer"
ACTS_CONTROLLER_REFERENCE_NAME = "iperf_servers"
def create(configs):
""" Factory method for iperf servers.
The function creates iperf servers based on at least one config.
If configs only specify a port number, a regular local IPerfServer object
will be created. If configs contains ssh settings or and AndroidDevice,
remote iperf servers will be started on those devices
Args:
config: config parameters for the iperf server
"""
results = []
for c in configs:
if type(c) is dict and "AndroidDevice" in c:
try:
results.append(IPerfServerOverAdb(c, logging.log_path))
except:
pass
elif type(c) is dict and "ssh_config" in c:
try:
results.append(IPerfServerOverSsh(c, logging.log_path))
except:
pass
else:
try:
results.append(IPerfServer(c, logging.log_path))
except:
pass
return results
def destroy(objs):
for ipf in objs:
try:
ipf.stop()
except:
pass
class IPerfResult(object):
def __init__(self, result_path):
""" Loads iperf result from file.
Loads iperf result from JSON formatted server log. File can be accessed
before or after server is stopped. Note that only the first JSON object
will be loaded and this funtion is not intended to be used with files
containing multiple iperf client runs.
"""
try:
with open(result_path, 'r') as f:
iperf_output = f.readlines()
if "}\n" in iperf_output:
iperf_output = iperf_output[0:
iperf_output.index("}\n") + 1]
iperf_string = ''.join(iperf_output)
iperf_string = iperf_string.replace("-nan", '0')
self.result = json.loads(iperf_string)
except ValueError:
with open(result_path, 'r') as f:
# Possibly a result from interrupted iperf run, skip first line
# and try again.
lines = f.readlines()[1:]
self.result = json.loads(''.join(lines))
def _has_data(self):
"""Checks if the iperf result has valid throughput data.
Returns:
True if the result contains throughput data. False otherwise.
"""
return ('end' in self.result) and ('sum_received' in self.result["end"]
or 'sum' in self.result["end"])
def get_json(self):
"""
Returns:
The raw json output from iPerf.
"""
return self.result
@property
def error(self):
if 'error' not in self.result:
return None
return self.result['error']
@property
def avg_rate(self):
"""Average UDP rate in MB/s over the entire run.
This is the average UDP rate observed at the terminal the iperf result
is pulled from. According to iperf3 documentation this is calculated
based on bytes sent and thus is not a good representation of the
quality of the link. If the result is not from a success run, this
property is None.
"""
if not self._has_data() or 'sum' not in self.result['end']:
return None
bps = self.result['end']['sum']['bits_per_second']
return bps / 8 / 1024 / 1024
@property
def avg_receive_rate(self):
"""Average receiving rate in MB/s over the entire run.
This data may not exist if iperf was interrupted. If the result is not
from a success run, this property is None.
"""
if not self._has_data() or 'sum_received' not in self.result['end']:
return None
bps = self.result['end']['sum_received']['bits_per_second']
return bps / 8 / 1024 / 1024
@property
def avg_send_rate(self):
"""Average sending rate in MB/s over the entire run.
This data may not exist if iperf was interrupted. If the result is not
from a success run, this property is None.
"""
if not self._has_data() or 'sum_sent' not in self.result['end']:
return None
bps = self.result['end']['sum_sent']['bits_per_second']
return bps / 8 / 1024 / 1024
@property
def instantaneous_rates(self):
"""Instantaneous received rate in MB/s over entire run.
This data may not exist if iperf was interrupted. If the result is not
from a success run, this property is None.
"""
if not self._has_data():
return None
intervals = [
interval["sum"]["bits_per_second"] / 8 / 1024 / 1024
for interval in self.result["intervals"]
]
return intervals
@property
def std_deviation(self):
"""Standard deviation of rates in MB/s over entire run.
This data may not exist if iperf was interrupted. If the result is not
from a success run, this property is None.
"""
return self.get_std_deviation(0)
def get_std_deviation(self, iperf_ignored_interval):
"""Standard deviation of rates in MB/s over entire run.
This data may not exist if iperf was interrupted. If the result is not
from a success run, this property is None. A configurable number of
beginning (and the single last) intervals are ignored in the
calculation as they are inaccurate (e.g. the last is from a very small
interval)
Args:
iperf_ignored_interval: number of iperf interval to ignored in
calculating standard deviation
"""
if not self._has_data():
return None
instantaneous_rates = self.instantaneous_rates[iperf_ignored_interval:
-1]
avg_rate = math.fsum(instantaneous_rates) / len(instantaneous_rates)
sqd_deviations = [(rate - avg_rate)**2 for rate in instantaneous_rates]
std_dev = math.sqrt(
math.fsum(sqd_deviations) / (len(sqd_deviations) - 1))
return std_dev
class IPerfServer():
"""Class that handles iperf3 operations.
"""
def __init__(self, config, log_path):
self.server_type = "local"
self.port = config
self.log_path = os.path.join(log_path, "iPerf{}".format(self.port))
utils.create_dir(self.log_path)
self.iperf_str = "iperf3 -s -J -p {}".format(self.port)
self.log_files = []
self.started = False
def start(self, extra_args="", tag=""):
"""Starts iperf server on local machine.
Args:
extra_args: A string representing extra arguments to start iperf
server with.
tag: Appended to log file name to identify logs from different
iperf runs.
"""
if self.started:
return
if tag:
tag = tag + ','
out_file_name = "IPerfServer,{},{}{}.log".format(
self.port, tag, len(self.log_files))
self.full_out_path = os.path.join(self.log_path, out_file_name)
cmd = "{} {} > {}".format(self.iperf_str, extra_args,
self.full_out_path)
self.iperf_process = utils.start_standing_subprocess(cmd)
self.log_files.append(self.full_out_path)
self.started = True
def stop(self):
""" Stops iperf server running.
"""
if not self.started:
return
utils.stop_standing_subprocess(self.iperf_process)
self.started = False
class IPerfServerOverSsh():
"""Class that handles iperf3 operations on remote machines.
"""
def __init__(self, config, log_path):
self.server_type = "remote"
self.ssh_settings = settings.from_config(config["ssh_config"])
self.ssh_session = connection.SshConnection(self.ssh_settings)
self.port = config["port"]
self.log_path = os.path.join(log_path, "iPerf{}".format(self.port))
utils.create_dir(self.log_path)
self.iperf_str = "iperf3 -s -J -p {}".format(self.port)
self.log_files = []
self.started = False
def start(self, extra_args="", tag=""):
"""Starts iperf server on specified machine and port.
Args:
extra_args: A string representing extra arguments to start iperf
server with.
tag: Appended to log file name to identify logs from different
iperf runs.
"""
if self.started:
return
if tag:
tag = tag + ','
out_file_name = "IPerfServer,{},{}{}.log".format(
self.port, tag, len(self.log_files))
self.full_out_path = os.path.join(self.log_path, out_file_name)
cmd = "{} {} > {}".format(self.iperf_str, extra_args,
"iperf_server_port{}.log".format(self.port))
job_result = self.ssh_session.run_async(cmd)
self.iperf_process = job_result.stdout
self.log_files.append(self.full_out_path)
self.started = True
def stop(self):
""" Stops iperf server running and gets output.
"""
if not self.started:
return
self.ssh_session.run_async("kill -9 {}".format(
str(self.iperf_process)))
iperf_result = self.ssh_session.run(
"cat iperf_server_port{}.log".format(self.port))
with open(self.full_out_path, 'w') as f:
f.write(iperf_result.stdout)
self.ssh_session.run_async("rm iperf_server_port{}.log".format(
self.port))
self.started = False
class IPerfServerOverAdb():
"""Class that handles iperf3 operations over ADB devices.
"""
def __init__(self, config, log_path):
# Note: skip_sl4a must be set to True in iperf server config since
# ACTS may have already initialized and started services on device
self.server_type = "adb"
self.adb_device = android_device.create(config["AndroidDevice"])
self.adb_device = self.adb_device[0]
self.adb_log_path = "~/data"
self.port = config["port"]
self.log_path = os.path.join(log_path, "iPerf{}".format(self.port))
utils.create_dir(self.log_path)
self.iperf_str = "iperf3 -s -J -p {}".format(self.port)
self.log_files = []
self.started = False
def start(self, extra_args="", tag=""):
"""Starts iperf server on an ADB device.
Args:
extra_args: A string representing extra arguments to start iperf
server with.
tag: Appended to log file name to identify logs from different
iperf runs.
"""
if self.started:
return
if tag:
tag = tag + ','
out_file_name = "IPerfServer,{},{}{}.log".format(
self.port, tag, len(self.log_files))
self.full_out_path = os.path.join(self.log_path, out_file_name)
cmd = "{} {} > {}/iperf_server_port{}.log".format(
self.iperf_str, extra_args, self.adb_log_path, self.port)
self.adb_device.adb.shell_nb(cmd)
self.iperf_process = self.adb_device.adb.shell("pgrep iperf3")
self.log_files.append(self.full_out_path)
self.started = True
def stop(self):
""" Stops iperf server running and gets output.
"""
if not self.started:
return
self.adb_device.adb.shell("kill -9 {}".format(self.iperf_process))
iperf_result = self.adb_device.adb.shell(
"cat {}/iperf_server_port{}.log".format(self.adb_log_path,
self.port))
with open(self.full_out_path, 'w') as f:
f.write(iperf_result)
self.adb_device.adb.shell("rm {}/iperf_server_port{}.log".format(
self.adb_log_path, self.port))
self.started = False