IperfUtility.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. import os
  2. import re
  3. import subprocess
  4. import time
  5. from builtins import object, range, str
  6. from idf_iperf_test_util import LineChart
  7. from tiny_test_fw import DUT, Utility
  8. try:
  9. from typing import Any, Tuple
  10. except ImportError:
  11. # Only used for type annotations
  12. pass
  13. # configurations
  14. TEST_TIME = TEST_TIMEOUT = 66
  15. WAIT_AP_POWER_ON_TIMEOUT = 90
  16. SCAN_TIMEOUT = 3
  17. SCAN_RETRY_COUNT = 3
  18. # constants
  19. FAILED_TO_SCAN_RSSI = -97
  20. INVALID_HEAP_SIZE = 0xFFFFFFFF
  21. PC_IPERF_TEMP_LOG_FILE = '.tmp_iperf.log'
  22. class TestResult(object):
  23. """ record, analysis test result and convert data to output format """
  24. PC_BANDWIDTH_LOG_PATTERN = re.compile(r'(\d+).0\s*-\s*(\d+).0\s+sec\s+[\d.]+\s+MBytes\s+([\d.]+)\s+Mbits/sec')
  25. DUT_BANDWIDTH_LOG_PATTERN = re.compile(r'(\d+)-\s+(\d+)\s+sec\s+([\d.]+)\s+Mbits/sec')
  26. ZERO_POINT_THRESHOLD = -88 # RSSI, dbm
  27. ZERO_THROUGHPUT_THRESHOLD = -92 # RSSI, dbm
  28. BAD_POINT_RSSI_THRESHOLD = -75 # RSSI, dbm
  29. BAD_POINT_MIN_THRESHOLD = 10 # Mbps
  30. BAD_POINT_PERCENTAGE_THRESHOLD = 0.3
  31. # we need at least 1/2 valid points to qualify the test result
  32. THROUGHPUT_QUALIFY_COUNT = TEST_TIME // 2
  33. RSSI_RANGE = [-x for x in range(10, 100)]
  34. ATT_RANGE = [x for x in range(0, 64)]
  35. def __init__(self, proto, direction, config_name): # type: (str, str, str) -> None
  36. self.proto = proto
  37. self.direction = direction
  38. self.config_name = config_name
  39. self.throughput_by_rssi = dict() # type: dict
  40. self.throughput_by_att = dict() # type: dict
  41. self.att_rssi_map = dict() # type: dict
  42. self.heap_size = INVALID_HEAP_SIZE
  43. self.error_list = [] # type: list[str]
  44. def _save_result(self, throughput, ap_ssid, att, rssi, heap_size): # type: (float, str, int, int, str) -> None
  45. """
  46. save the test results:
  47. * record the better throughput if att/rssi is the same.
  48. * record the min heap size.
  49. """
  50. if ap_ssid not in self.att_rssi_map:
  51. # for new ap, create empty dict()
  52. self.throughput_by_att[ap_ssid] = dict()
  53. self.throughput_by_rssi[ap_ssid] = dict()
  54. self.att_rssi_map[ap_ssid] = dict()
  55. self.att_rssi_map[ap_ssid][att] = rssi
  56. def record_throughput(database, key_value): # type: (dict, int) -> None
  57. try:
  58. # we save the larger value for same att
  59. if throughput > database[ap_ssid][key_value]:
  60. database[ap_ssid][key_value] = throughput
  61. except KeyError:
  62. database[ap_ssid][key_value] = throughput
  63. record_throughput(self.throughput_by_att, att)
  64. record_throughput(self.throughput_by_rssi, rssi)
  65. if int(heap_size) < self.heap_size:
  66. self.heap_size = int(heap_size)
  67. def add_result(self, raw_data, ap_ssid, att, rssi, heap_size): # type: (str, str, int, int, str) -> float
  68. """
  69. add result for one test
  70. :param raw_data: iperf raw data
  71. :param ap_ssid: ap ssid that tested
  72. :param att: attenuate value
  73. :param rssi: AP RSSI
  74. :param heap_size: min heap size during test
  75. :return: throughput
  76. """
  77. fall_to_0_recorded = 0
  78. throughput_list = []
  79. throughput = 0.0
  80. result_list = self.PC_BANDWIDTH_LOG_PATTERN.findall(raw_data)
  81. if not result_list:
  82. # failed to find raw data by PC pattern, it might be DUT pattern
  83. result_list = self.DUT_BANDWIDTH_LOG_PATTERN.findall(raw_data)
  84. for result in result_list:
  85. if int(result[1]) - int(result[0]) != 1:
  86. # this could be summary, ignore this
  87. continue
  88. throughput_list.append(float(result[2]))
  89. throughput = (throughput if (throughput > float(result[2])) else float(result[2]))
  90. if float(result[2]) == 0 and rssi > self.ZERO_POINT_THRESHOLD \
  91. and fall_to_0_recorded < 1:
  92. # throughput fall to 0 error. we only record 1 records for one test
  93. self.error_list.append('[Error][fall to 0][{}][att: {}][rssi: {}]: 0 throughput interval: {}-{}'
  94. .format(ap_ssid, att, rssi, result[0], result[1]))
  95. fall_to_0_recorded += 1
  96. if len(throughput_list) < self.THROUGHPUT_QUALIFY_COUNT:
  97. throughput = 0.0
  98. if throughput == 0 and rssi > self.ZERO_THROUGHPUT_THRESHOLD:
  99. self.error_list.append('[Error][Fatal][{}][att: {}][rssi: {}]: No throughput data found'
  100. .format(ap_ssid, att, rssi))
  101. self._save_result(throughput, ap_ssid, att, rssi, heap_size)
  102. return throughput
  103. def post_analysis(self): # type: () -> None
  104. """
  105. some rules need to be checked after we collected all test raw data:
  106. 1. throughput value 30% worse than the next point with lower RSSI
  107. 2. throughput value 30% worse than the next point with larger attenuate
  108. """
  109. def analysis_bad_point(data, index_type): # type: (dict, str) -> None
  110. for ap_ssid in data:
  111. result_dict = data[ap_ssid]
  112. index_list = list(result_dict.keys())
  113. index_list.sort()
  114. if index_type == 'att':
  115. index_list.reverse()
  116. for i, index_value in enumerate(index_list[1:]):
  117. if index_value < self.BAD_POINT_RSSI_THRESHOLD or \
  118. result_dict[index_list[i]] < self.BAD_POINT_MIN_THRESHOLD:
  119. continue
  120. _percentage = result_dict[index_value] / result_dict[index_list[i]]
  121. if _percentage < 1 - self.BAD_POINT_PERCENTAGE_THRESHOLD:
  122. self.error_list.append('[Error][Bad point][{}][{}: {}]: drop {:.02f}%'
  123. .format(ap_ssid, index_type, index_value,
  124. (1 - _percentage) * 100))
  125. analysis_bad_point(self.throughput_by_rssi, 'rssi')
  126. analysis_bad_point(self.throughput_by_att, 'att')
  127. def draw_throughput_figure(self, path, ap_ssid, draw_type): # type: (str, str, str) -> str
  128. """
  129. :param path: folder to save figure. make sure the folder is already created.
  130. :param ap_ssid: ap ssid string or a list of ap ssid string
  131. :param draw_type: "att" or "rssi"
  132. :return: file_name
  133. """
  134. if draw_type == 'rssi':
  135. type_name = 'RSSI'
  136. data = self.throughput_by_rssi
  137. range_list = self.RSSI_RANGE
  138. elif draw_type == 'att':
  139. type_name = 'Att'
  140. data = self.throughput_by_att
  141. range_list = self.ATT_RANGE
  142. else:
  143. raise AssertionError('draw type not supported')
  144. if isinstance(ap_ssid, list):
  145. file_name = 'ThroughputVs{}_{}_{}.html'.format(type_name, self.proto, self.direction)
  146. else:
  147. file_name = 'ThroughputVs{}_{}_{}.html'.format(type_name, self.proto, self.direction)
  148. LineChart.draw_line_chart(os.path.join(path, file_name),
  149. 'Throughput Vs {} ({} {})'.format(type_name, self.proto, self.direction),
  150. '{} (dbm)'.format(type_name),
  151. 'Throughput (Mbps)',
  152. data, range_list)
  153. return file_name
  154. def draw_rssi_vs_att_figure(self, path, ap_ssid): # type: (str, str) -> str
  155. """
  156. :param path: folder to save figure. make sure the folder is already created.
  157. :param ap_ssid: ap to use
  158. :return: file_name
  159. """
  160. if isinstance(ap_ssid, list):
  161. file_name = 'AttVsRSSI.html'
  162. else:
  163. file_name = 'AttVsRSSI.html'
  164. LineChart.draw_line_chart(os.path.join(path, file_name),
  165. 'Att Vs RSSI',
  166. 'Att (dbm)',
  167. 'RSSI (dbm)',
  168. self.att_rssi_map,
  169. self.ATT_RANGE)
  170. return file_name
  171. def get_best_throughput(self): # type: () -> Any
  172. """ get the best throughput during test """
  173. best_for_aps = [max(self.throughput_by_att[ap_ssid].values())
  174. for ap_ssid in self.throughput_by_att]
  175. return max(best_for_aps)
  176. def __str__(self): # type: () -> str
  177. """
  178. returns summary for this test:
  179. 1. test result (success or fail)
  180. 2. best performance for each AP
  181. 3. min free heap size during test
  182. """
  183. if self.throughput_by_att:
  184. ret = '[{}_{}][{}]: {}\r\n\r\n'.format(self.proto, self.direction, self.config_name,
  185. 'Fail' if self.error_list else 'Success')
  186. ret += 'Performance for each AP:\r\n'
  187. for ap_ssid in self.throughput_by_att:
  188. ret += '[{}]: {:.02f} Mbps\r\n'.format(ap_ssid, max(self.throughput_by_att[ap_ssid].values()))
  189. if self.heap_size != INVALID_HEAP_SIZE:
  190. ret += 'Minimum heap size: {}'.format(self.heap_size)
  191. else:
  192. ret = ''
  193. return ret
  194. class IperfTestUtility(object):
  195. """ iperf test implementation """
  196. def __init__(self, dut, config_name, ap_ssid, ap_password,
  197. pc_nic_ip, pc_iperf_log_file, test_result=None): # type: (str, str, str, str, str, str, Any) -> None
  198. self.config_name = config_name
  199. self.dut = dut
  200. self.pc_iperf_log_file = pc_iperf_log_file
  201. self.ap_ssid = ap_ssid
  202. self.ap_password = ap_password
  203. self.pc_nic_ip = pc_nic_ip
  204. self.fail_to_scan = 0
  205. self.lowest_rssi_scanned = 0
  206. if test_result:
  207. self.test_result = test_result
  208. else:
  209. self.test_result = {
  210. 'tcp_tx': TestResult('tcp', 'tx', config_name),
  211. 'tcp_rx': TestResult('tcp', 'rx', config_name),
  212. 'udp_tx': TestResult('udp', 'tx', config_name),
  213. 'udp_rx': TestResult('udp', 'rx', config_name),
  214. }
  215. def setup(self): # type: (Any) -> Tuple[str,int]
  216. """
  217. setup iperf test:
  218. 1. kill current iperf process
  219. 2. reboot DUT (currently iperf is not very robust, need to reboot DUT)
  220. 3. scan to get AP RSSI
  221. 4. connect to AP
  222. """
  223. try:
  224. subprocess.check_output('sudo killall iperf 2>&1 > /dev/null', shell=True)
  225. except subprocess.CalledProcessError:
  226. pass
  227. time.sleep(5)
  228. self.dut.write('restart')
  229. self.dut.expect_any('iperf>', 'esp32>')
  230. self.dut.write('scan {}'.format(self.ap_ssid))
  231. for _ in range(SCAN_RETRY_COUNT):
  232. try:
  233. rssi = int(self.dut.expect(re.compile(r'\[{}]\[rssi=(-\d+)]'.format(self.ap_ssid)),
  234. timeout=SCAN_TIMEOUT)[0])
  235. break
  236. except DUT.ExpectTimeout:
  237. continue
  238. else:
  239. raise AssertionError('Failed to scan AP')
  240. self.dut.write('sta {} {}'.format(self.ap_ssid, self.ap_password))
  241. dut_ip = self.dut.expect(re.compile(r'sta ip: ([\d.]+), mask: ([\d.]+), gw: ([\d.]+)'))[0]
  242. return dut_ip, rssi
  243. def _save_test_result(self, test_case, raw_data, att, rssi, heap_size): # type: (str, str, int, int, int) -> Any
  244. return self.test_result[test_case].add_result(raw_data, self.ap_ssid, att, rssi, heap_size)
  245. def _test_once(self, proto, direction): # type: (Any, str, str) -> Tuple[str, int, int]
  246. """ do measure once for one type """
  247. # connect and scan to get RSSI
  248. dut_ip, rssi = self.setup()
  249. assert direction in ['rx', 'tx']
  250. assert proto in ['tcp', 'udp']
  251. # run iperf test
  252. if direction == 'tx':
  253. with open(PC_IPERF_TEMP_LOG_FILE, 'w') as f:
  254. if proto == 'tcp':
  255. process = subprocess.Popen(['iperf', '-s', '-B', self.pc_nic_ip,
  256. '-t', str(TEST_TIME), '-i', '1', '-f', 'm'],
  257. stdout=f, stderr=f)
  258. self.dut.write('iperf -c {} -i 1 -t {}'.format(self.pc_nic_ip, TEST_TIME))
  259. else:
  260. process = subprocess.Popen(['iperf', '-s', '-u', '-B', self.pc_nic_ip,
  261. '-t', str(TEST_TIME), '-i', '1', '-f', 'm'],
  262. stdout=f, stderr=f)
  263. self.dut.write('iperf -c {} -u -i 1 -t {}'.format(self.pc_nic_ip, TEST_TIME))
  264. for _ in range(TEST_TIMEOUT):
  265. if process.poll() is not None:
  266. break
  267. time.sleep(1)
  268. else:
  269. process.terminate()
  270. with open(PC_IPERF_TEMP_LOG_FILE, 'r') as f:
  271. pc_raw_data = server_raw_data = f.read()
  272. else:
  273. with open(PC_IPERF_TEMP_LOG_FILE, 'w') as f:
  274. if proto == 'tcp':
  275. self.dut.write('iperf -s -i 1 -t {}'.format(TEST_TIME))
  276. # wait until DUT TCP server created
  277. try:
  278. self.dut.expect('iperf tcp server create successfully', timeout=1)
  279. except DUT.ExpectTimeout:
  280. # compatible with old iperf example binary
  281. Utility.console_log('create iperf tcp server fail')
  282. process = subprocess.Popen(['iperf', '-c', dut_ip,
  283. '-t', str(TEST_TIME), '-f', 'm'],
  284. stdout=f, stderr=f)
  285. for _ in range(TEST_TIMEOUT):
  286. if process.poll() is not None:
  287. break
  288. time.sleep(1)
  289. else:
  290. process.terminate()
  291. else:
  292. self.dut.write('iperf -s -u -i 1 -t {}'.format(TEST_TIME))
  293. # wait until DUT TCP server created
  294. try:
  295. self.dut.expect('iperf udp server create successfully', timeout=1)
  296. except DUT.ExpectTimeout:
  297. # compatible with old iperf example binary
  298. Utility.console_log('create iperf udp server fail')
  299. for bandwidth in range(50, 101, 5):
  300. process = subprocess.Popen(['iperf', '-c', dut_ip, '-u', '-b', str(bandwidth) + 'm',
  301. '-t', str(TEST_TIME / 11), '-f', 'm'], stdout=f, stderr=f)
  302. for _ in range(TEST_TIMEOUT):
  303. if process.poll() is not None:
  304. break
  305. time.sleep(1)
  306. else:
  307. process.terminate()
  308. server_raw_data = self.dut.read()
  309. with open(PC_IPERF_TEMP_LOG_FILE, 'r') as f:
  310. pc_raw_data = f.read()
  311. # save PC iperf logs to console
  312. with open(self.pc_iperf_log_file, 'a+') as f:
  313. f.write('## [{}] `{}`\r\n##### {}'
  314. .format(self.config_name,
  315. '{}_{}'.format(proto, direction),
  316. time.strftime('%m-%d %H:%M:%S', time.localtime(time.time()))))
  317. f.write('\r\n```\r\n\r\n' + pc_raw_data + '\r\n```\r\n')
  318. self.dut.write('heap')
  319. heap_size = self.dut.expect(re.compile(r'min heap size: (\d+)\D'))[0]
  320. # return server raw data (for parsing test results) and RSSI
  321. return server_raw_data, rssi, heap_size
  322. def run_test(self, proto, direction, atten_val): # type: (str, str, int) -> None
  323. """
  324. run test for one type, with specified atten_value and save the test result
  325. :param proto: tcp or udp
  326. :param direction: tx or rx
  327. :param atten_val: attenuate value
  328. """
  329. rssi = FAILED_TO_SCAN_RSSI
  330. heap_size = INVALID_HEAP_SIZE
  331. try:
  332. server_raw_data, rssi, heap_size = self._test_once(proto, direction)
  333. throughput = self._save_test_result('{}_{}'.format(proto, direction),
  334. server_raw_data, atten_val,
  335. rssi, heap_size)
  336. Utility.console_log('[{}][{}_{}][{}][{}]: {:.02f}'
  337. .format(self.config_name, proto, direction, rssi, self.ap_ssid, throughput))
  338. self.lowest_rssi_scanned = min(self.lowest_rssi_scanned, rssi)
  339. except (ValueError, IndexError):
  340. self._save_test_result('{}_{}'.format(proto, direction), '', atten_val, rssi, heap_size)
  341. Utility.console_log('Fail to get throughput results.')
  342. except AssertionError:
  343. self.fail_to_scan += 1
  344. Utility.console_log('Fail to scan AP.')
  345. def run_all_cases(self, atten_val): # type: (int) -> None
  346. """
  347. run test for all types (udp_tx, udp_rx, tcp_tx, tcp_rx).
  348. :param atten_val: attenuate value
  349. """
  350. self.run_test('tcp', 'tx', atten_val)
  351. self.run_test('tcp', 'rx', atten_val)
  352. self.run_test('udp', 'tx', atten_val)
  353. self.run_test('udp', 'rx', atten_val)
  354. if self.fail_to_scan > 10:
  355. Utility.console_log(
  356. 'Fail to scan AP for more than 10 times. Lowest RSSI scanned is {}'.format(self.lowest_rssi_scanned))
  357. raise AssertionError
  358. def wait_ap_power_on(self): # type: (Any) -> bool
  359. """
  360. AP need to take sometime to power on. It changes for different APs.
  361. This method will scan to check if the AP powers on.
  362. :return: True or False
  363. """
  364. self.dut.write('restart')
  365. self.dut.expect_any('iperf>', 'esp32>')
  366. for _ in range(WAIT_AP_POWER_ON_TIMEOUT // SCAN_TIMEOUT):
  367. try:
  368. self.dut.write('scan {}'.format(self.ap_ssid))
  369. self.dut.expect(re.compile(r'\[{}]\[rssi=(-\d+)]'.format(self.ap_ssid)),
  370. timeout=SCAN_TIMEOUT)
  371. ret = True
  372. break
  373. except DUT.ExpectTimeout:
  374. pass
  375. else:
  376. ret = False
  377. return ret