Home | History | Annotate | Download | only in controllers
      1 #!/usr/bin/env python3.4
      2 #
      3 #   Copyright 2016 - The Android Open Source Project
      4 #
      5 #   Licensed under the Apache License, Version 2.0 (the "License");
      6 #   you may not use this file except in compliance with the License.
      7 #   You may obtain a copy of the License at
      8 #
      9 #       http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 #   Unless required by applicable law or agreed to in writing, software
     12 #   distributed under the License is distributed on an "AS IS" BASIS,
     13 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 #   See the License for the specific language governing permissions and
     15 #   limitations under the License.
     16 
     17 import json
     18 import logging
     19 import math
     20 import os
     21 from acts import utils
     22 from acts.controllers import android_device
     23 from acts.controllers.utils_lib.ssh import connection
     24 from acts.controllers.utils_lib.ssh import settings
     25 
     26 ACTS_CONTROLLER_CONFIG_NAME = "IPerfServer"
     27 ACTS_CONTROLLER_REFERENCE_NAME = "iperf_servers"
     28 
     29 
     30 def create(configs):
     31     """ Factory method for iperf servers.
     32 
     33     The function creates iperf servers based on at least one config.
     34     If configs only specify a port number, a regular local IPerfServer object
     35     will be created. If configs contains ssh settings or and AndroidDevice,
     36     remote iperf servers will be started on those devices
     37 
     38     Args:
     39         config: config parameters for the iperf server
     40     """
     41     results = []
     42     for c in configs:
     43         if type(c) is dict and "AndroidDevice" in c:
     44             try:
     45                 results.append(IPerfServerOverAdb(c, logging.log_path))
     46             except:
     47                 pass
     48         elif type(c) is dict and "ssh_config" in c:
     49             try:
     50                 results.append(IPerfServerOverSsh(c, logging.log_path))
     51             except:
     52                 pass
     53         else:
     54             try:
     55                 results.append(IPerfServer(c, logging.log_path))
     56             except:
     57                 pass
     58     return results
     59 
     60 
     61 def destroy(objs):
     62     for ipf in objs:
     63         try:
     64             ipf.stop()
     65         except:
     66             pass
     67 
     68 
     69 class IPerfResult(object):
     70     def __init__(self, result_path):
     71         """ Loads iperf result from file.
     72 
     73         Loads iperf result from JSON formatted server log. File can be accessed
     74         before or after server is stopped. Note that only the first JSON object
     75         will be loaded and this funtion is not intended to be used with files
     76         containing multiple iperf client runs.
     77         """
     78         try:
     79             with open(result_path, 'r') as f:
     80                 iperf_output = f.readlines()
     81                 if "}\n" in iperf_output:
     82                     iperf_output = iperf_output[0:
     83                                                 iperf_output.index("}\n") + 1]
     84                 iperf_string = ''.join(iperf_output)
     85                 iperf_string = iperf_string.replace("-nan", '0')
     86                 self.result = json.loads(iperf_string)
     87         except ValueError:
     88             with open(result_path, 'r') as f:
     89                 # Possibly a result from interrupted iperf run, skip first line
     90                 # and try again.
     91                 lines = f.readlines()[1:]
     92                 self.result = json.loads(''.join(lines))
     93 
     94     def _has_data(self):
     95         """Checks if the iperf result has valid throughput data.
     96 
     97         Returns:
     98             True if the result contains throughput data. False otherwise.
     99         """
    100         return ('end' in self.result) and ('sum_received' in self.result["end"]
    101                                            or 'sum' in self.result["end"])
    102 
    103     def get_json(self):
    104         """
    105         Returns:
    106             The raw json output from iPerf.
    107         """
    108         return self.result
    109 
    110     @property
    111     def error(self):
    112         if 'error' not in self.result:
    113             return None
    114         return self.result['error']
    115 
    116     @property
    117     def avg_rate(self):
    118         """Average UDP rate in MB/s over the entire run.
    119 
    120         This is the average UDP rate observed at the terminal the iperf result
    121         is pulled from. According to iperf3 documentation this is calculated
    122         based on bytes sent and thus is not a good representation of the
    123         quality of the link. If the result is not from a success run, this
    124         property is None.
    125         """
    126         if not self._has_data() or 'sum' not in self.result['end']:
    127             return None
    128         bps = self.result['end']['sum']['bits_per_second']
    129         return bps / 8 / 1024 / 1024
    130 
    131     @property
    132     def avg_receive_rate(self):
    133         """Average receiving rate in MB/s over the entire run.
    134 
    135         This data may not exist if iperf was interrupted. If the result is not
    136         from a success run, this property is None.
    137         """
    138         if not self._has_data() or 'sum_received' not in self.result['end']:
    139             return None
    140         bps = self.result['end']['sum_received']['bits_per_second']
    141         return bps / 8 / 1024 / 1024
    142 
    143     @property
    144     def avg_send_rate(self):
    145         """Average sending rate in MB/s over the entire run.
    146 
    147         This data may not exist if iperf was interrupted. If the result is not
    148         from a success run, this property is None.
    149         """
    150         if not self._has_data() or 'sum_sent' not in self.result['end']:
    151             return None
    152         bps = self.result['end']['sum_sent']['bits_per_second']
    153         return bps / 8 / 1024 / 1024
    154 
    155     @property
    156     def instantaneous_rates(self):
    157         """Instantaneous received rate in MB/s over entire run.
    158 
    159         This data may not exist if iperf was interrupted. If the result is not
    160         from a success run, this property is None.
    161         """
    162         if not self._has_data():
    163             return None
    164         intervals = [
    165             interval["sum"]["bits_per_second"] / 8 / 1024 / 1024
    166             for interval in self.result["intervals"]
    167         ]
    168         return intervals
    169 
    170     @property
    171     def std_deviation(self):
    172         """Standard deviation of rates in MB/s over entire run.
    173 
    174         This data may not exist if iperf was interrupted. If the result is not
    175         from a success run, this property is None.
    176         """
    177         return self.get_std_deviation(0)
    178 
    179     def get_std_deviation(self, iperf_ignored_interval):
    180         """Standard deviation of rates in MB/s over entire run.
    181 
    182         This data may not exist if iperf was interrupted. If the result is not
    183         from a success run, this property is None. A configurable number of
    184         beginning (and the single last) intervals are ignored in the
    185         calculation as they are inaccurate (e.g. the last is from a very small
    186         interval)
    187 
    188         Args:
    189             iperf_ignored_interval: number of iperf interval to ignored in
    190             calculating standard deviation
    191         """
    192         if not self._has_data():
    193             return None
    194         instantaneous_rates = self.instantaneous_rates[iperf_ignored_interval:
    195                                                        -1]
    196         avg_rate = math.fsum(instantaneous_rates) / len(instantaneous_rates)
    197         sqd_deviations = [(rate - avg_rate)**2 for rate in instantaneous_rates]
    198         std_dev = math.sqrt(
    199             math.fsum(sqd_deviations) / (len(sqd_deviations) - 1))
    200         return std_dev
    201 
    202 
    203 class IPerfServer():
    204     """Class that handles iperf3 operations.
    205 
    206     """
    207 
    208     def __init__(self, config, log_path):
    209         self.server_type = "local"
    210         self.port = config
    211         self.log_path = os.path.join(log_path, "iPerf{}".format(self.port))
    212         utils.create_dir(self.log_path)
    213         self.iperf_str = "iperf3 -s -J -p {}".format(self.port)
    214         self.log_files = []
    215         self.started = False
    216 
    217     def start(self, extra_args="", tag=""):
    218         """Starts iperf server on local machine.
    219 
    220         Args:
    221             extra_args: A string representing extra arguments to start iperf
    222                 server with.
    223             tag: Appended to log file name to identify logs from different
    224                 iperf runs.
    225         """
    226         if self.started:
    227             return
    228         if tag:
    229             tag = tag + ','
    230         out_file_name = "IPerfServer,{},{}{}.log".format(
    231             self.port, tag, len(self.log_files))
    232         self.full_out_path = os.path.join(self.log_path, out_file_name)
    233         cmd = "{} {} > {}".format(self.iperf_str, extra_args,
    234                                   self.full_out_path)
    235         self.iperf_process = utils.start_standing_subprocess(cmd)
    236         self.log_files.append(self.full_out_path)
    237         self.started = True
    238 
    239     def stop(self):
    240         """ Stops iperf server running.
    241 
    242         """
    243         if not self.started:
    244             return
    245         utils.stop_standing_subprocess(self.iperf_process)
    246         self.started = False
    247 
    248 
    249 class IPerfServerOverSsh():
    250     """Class that handles iperf3 operations on remote machines.
    251 
    252     """
    253 
    254     def __init__(self, config, log_path):
    255         self.server_type = "remote"
    256         self.ssh_settings = settings.from_config(config["ssh_config"])
    257         self.ssh_session = connection.SshConnection(self.ssh_settings)
    258         self.port = config["port"]
    259         self.log_path = os.path.join(log_path, "iPerf{}".format(self.port))
    260         utils.create_dir(self.log_path)
    261         self.iperf_str = "iperf3 -s -J -p {}".format(self.port)
    262         self.log_files = []
    263         self.started = False
    264 
    265     def start(self, extra_args="", tag=""):
    266         """Starts iperf server on specified machine and port.
    267 
    268         Args:
    269             extra_args: A string representing extra arguments to start iperf
    270                 server with.
    271             tag: Appended to log file name to identify logs from different
    272                 iperf runs.
    273         """
    274         if self.started:
    275             return
    276         if tag:
    277             tag = tag + ','
    278         out_file_name = "IPerfServer,{},{}{}.log".format(
    279             self.port, tag, len(self.log_files))
    280         self.full_out_path = os.path.join(self.log_path, out_file_name)
    281         cmd = "{} {} > {}".format(self.iperf_str, extra_args,
    282                                   "iperf_server_port{}.log".format(self.port))
    283         job_result = self.ssh_session.run_async(cmd)
    284         self.iperf_process = job_result.stdout
    285         self.log_files.append(self.full_out_path)
    286         self.started = True
    287 
    288     def stop(self):
    289         """ Stops iperf server running and gets output.
    290 
    291         """
    292         if not self.started:
    293             return
    294         self.ssh_session.run_async("kill -9 {}".format(
    295             str(self.iperf_process)))
    296         iperf_result = self.ssh_session.run(
    297             "cat iperf_server_port{}.log".format(self.port))
    298         with open(self.full_out_path, 'w') as f:
    299             f.write(iperf_result.stdout)
    300         self.ssh_session.run_async("rm iperf_server_port{}.log".format(
    301             self.port))
    302         self.started = False
    303 
    304 
    305 class IPerfServerOverAdb():
    306     """Class that handles iperf3 operations over ADB devices.
    307 
    308     """
    309 
    310     def __init__(self, config, log_path):
    311 
    312         # Note: skip_sl4a must be set to True in iperf server config since
    313         # ACTS may have already initialized and started services on device
    314         self.server_type = "adb"
    315         self.adb_device = android_device.create(config["AndroidDevice"])
    316         self.adb_device = self.adb_device[0]
    317         self.adb_log_path = "~/data"
    318         self.port = config["port"]
    319         self.log_path = os.path.join(log_path, "iPerf{}".format(self.port))
    320         utils.create_dir(self.log_path)
    321         self.iperf_str = "iperf3 -s -J -p {}".format(self.port)
    322         self.log_files = []
    323         self.started = False
    324 
    325     def start(self, extra_args="", tag=""):
    326         """Starts iperf server on an ADB device.
    327 
    328         Args:
    329             extra_args: A string representing extra arguments to start iperf
    330                 server with.
    331             tag: Appended to log file name to identify logs from different
    332                 iperf runs.
    333         """
    334         if self.started:
    335             return
    336         if tag:
    337             tag = tag + ','
    338         out_file_name = "IPerfServer,{},{}{}.log".format(
    339             self.port, tag, len(self.log_files))
    340         self.full_out_path = os.path.join(self.log_path, out_file_name)
    341         cmd = "{} {} > {}/iperf_server_port{}.log".format(
    342             self.iperf_str, extra_args, self.adb_log_path, self.port)
    343         self.adb_device.adb.shell_nb(cmd)
    344         self.iperf_process = self.adb_device.adb.shell("pgrep iperf3")
    345         self.log_files.append(self.full_out_path)
    346         self.started = True
    347 
    348     def stop(self):
    349         """ Stops iperf server running and gets output.
    350 
    351         """
    352         if not self.started:
    353             return
    354         self.adb_device.adb.shell("kill -9 {}".format(self.iperf_process))
    355         iperf_result = self.adb_device.adb.shell(
    356             "cat {}/iperf_server_port{}.log".format(self.adb_log_path,
    357                                                     self.port))
    358         with open(self.full_out_path, 'w') as f:
    359             f.write(iperf_result)
    360         self.adb_device.adb.shell("rm {}/iperf_server_port{}.log".format(
    361             self.adb_log_path, self.port))
    362         self.started = False
    363