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