IperfUtility.py 20 KB

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