blob: bdbc303d1ff3dc96ac81c6643a04a47b0ec113b0 [file] [log] [blame]
Daniele Moro4a5a91f2021-09-07 17:24:39 +02001"""
2Copyright 2021 Open Networking Foundation (ONF)
3
4Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>,
5the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>,
6or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg>
7
8"""
9import time
10import os
Daniele Morob8404e82022-02-25 00:17:28 +010011import copy
Daniele Moro732055a2021-09-28 15:28:46 +020012import sys
13import importlib
Daniele Moro4a5a91f2021-09-07 17:24:39 +020014import collections
15import numpy as np
16
17from drivers.common.api.controllerdriver import Controller
Daniele Moro4a5a91f2021-09-07 17:24:39 +020018
19from socket import error as ConnectionRefusedError
20from distutils.util import strtobool
21
22TREX_FILES_DIR = "/tmp/trex_files/"
23
24LatencyStats = collections.namedtuple(
25 "LatencyStats",
26 [
27 "pg_id",
28 "jitter",
29 "average",
30 "total_max",
31 "total_min",
32 "last_max",
33 "histogram",
34 "dropped",
35 "out_of_order",
36 "duplicate",
37 "seq_too_high",
38 "seq_too_low",
39 "percentile_50",
40 "percentile_75",
41 "percentile_90",
42 "percentile_99",
43 "percentile_99_9",
44 "percentile_99_99",
45 "percentile_99_999",
46 ],
47)
48
49PortStats = collections.namedtuple(
50 "PortStats",
51 [
52 "tx_packets",
53 "rx_packets",
54 "tx_bytes",
55 "rx_bytes",
56 "tx_errors",
57 "rx_errors",
58 "tx_bps",
59 "tx_pps",
60 "tx_bps_L1",
61 "tx_util",
62 "rx_bps",
63 "rx_pps",
64 "rx_bps_L1",
65 "rx_util",
66 ],
67)
68
69FlowStats = collections.namedtuple(
70 "FlowStats",
71 [
72 "pg_id",
73 "tx_packets",
74 "rx_packets",
75 "tx_bytes",
76 "rx_bytes",
77 ],
78)
79
80
81class TrexClientDriver(Controller):
82 """
83 Implements a Trex Client Driver
84 """
85
86 def __init__(self):
87 self.trex_address = "localhost"
88 self.trex_config = None # Relative path in dependencies of the test using this driver
89 self.force_restart = True
90 self.sofware_mode = False
91 self.setup_successful = False
92 self.stats = None
93 self.trex_client = None
94 self.trex_daemon_client = None
Daniele Moro732055a2021-09-28 15:28:46 +020095 self.trex_library_python_path = None
Daniele Morob8404e82022-02-25 00:17:28 +010096 self.gen_traffic_per_port = {}
Daniele Moro4a5a91f2021-09-07 17:24:39 +020097 super(TrexClientDriver, self).__init__()
98
99 def connect(self, **connectargs):
Daniele Moro732055a2021-09-28 15:28:46 +0200100 global STLClient, STLStreamDstMAC_PKT, CTRexClient, STLPktBuilder, \
101 STLFlowLatencyStats, STLStream, STLTXCont
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200102 try:
103 for key in connectargs:
104 vars(self)[key] = connectargs[key]
105 for key in self.options:
106 if key == "trex_address":
107 self.trex_address = self.options[key]
108 elif key == "trex_config":
109 self.trex_config = self.options[key]
110 elif key == "force_restart":
111 self.force_restart = bool(strtobool(self.options[key]))
112 elif key == "software_mode":
113 self.software_mode = bool(strtobool(self.options[key]))
Daniele Moro732055a2021-09-28 15:28:46 +0200114 elif key == "trex_library_python_path":
115 self.trex_library_python_path = self.options[key]
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200116 self.name = self.options["name"]
Daniele Moro732055a2021-09-28 15:28:46 +0200117 if self.trex_library_python_path is not None:
118 sys.path.append(self.trex_library_python_path)
119 # Import after appending the TRex library Python path
120 STLClient = getattr(importlib.import_module("trex.stl.api"), "STLClient")
121 STLStreamDstMAC_PKT = getattr(importlib.import_module("trex.stl.api"), "STLStreamDstMAC_PKT")
122 CTRexClient = getattr(importlib.import_module("trex_stf_lib.trex_client"), "CTRexClient")
123 STLFlowLatencyStats = getattr(importlib.import_module("trex_stl_lib.api"), "STLFlowLatencyStats")
124 STLPktBuilder = getattr(importlib.import_module("trex_stl_lib.api"), "STLPktBuilder")
125 STLStream = getattr(importlib.import_module("trex_stl_lib.api"), "STLStream")
126 STLTXCont = getattr(importlib.import_module("trex_stl_lib.api"), "STLTXCont")
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200127 except Exception as inst:
128 main.log.error("Uncaught exception: " + str(inst))
129 main.cleanAndExit()
130 return super(TrexClientDriver, self).connect()
131
132 def disconnect(self):
133 """
134 Called when Test is complete
135 """
136 self.disconnectTrexClient()
137 self.stopTrexServer()
138 return main.TRUE
139
140 def setupTrex(self, pathToTrexConfig):
141 """
142 Setup TRex server passing the TRex configuration.
143 :return: True if setup successful, False otherwise
144 """
145 main.log.debug(self.name + ": Setting up TRex server")
146 if self.software_mode:
147 trex_args = "--software --no-hw-flow-stat"
148 else:
149 trex_args = None
150 self.trex_daemon_client = CTRexClient(self.trex_address,
151 trex_args=trex_args)
152 success = self.__set_up_trex_server(
153 self.trex_daemon_client, self.trex_address,
Yi Tsengdda7e322021-09-20 14:21:20 -0700154 os.path.join(pathToTrexConfig, self.trex_config),
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200155 self.force_restart
156 )
157 if not success:
158 main.log.error("Failed to set up TRex daemon!")
159 return False
160 self.setup_successful = True
161 return True
162
163 def connectTrexClient(self):
164 if not self.setup_successful:
165 main.log.error("Cannot connect TRex Client, first setup TRex")
166 return False
167 main.log.info("Connecting TRex Client")
168 self.trex_client = STLClient(server=self.trex_address)
169 self.trex_client.connect()
170 self.trex_client.acquire()
171 self.trex_client.reset() # Resets configs from all ports
172 self.trex_client.clear_stats() # Clear status from all ports
173 # Put all ports to promiscuous mode, otherwise they will drop all
174 # incoming packets if the destination mac is not the port mac address.
175 self.trex_client.set_port_attr(self.trex_client.get_all_ports(),
176 promiscuous=True)
Daniele Morob8404e82022-02-25 00:17:28 +0100177 self.gen_traffic_per_port = {}
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200178 self.stats = None
179 return True
180
181 def disconnectTrexClient(self):
182 # Teardown TREX Client
183 if self.trex_client is not None:
184 main.log.info("Tearing down STLClient...")
185 self.trex_client.stop()
186 self.trex_client.release()
187 self.trex_client.disconnect()
188 self.trex_client = None
189 # Do not reset stats
190
191 def stopTrexServer(self):
192 if self.trex_daemon_client is not None:
193 self.trex_daemon_client.stop_trex()
194 self.trex_daemon_client = None
195
196 def addStream(self, pkt, trex_port, l1_bps=None, percentage=None,
197 delay=0, flow_id=None, flow_stats=False):
198 """
199 :param pkt: Scapy packet, TRex will send copy of this packet
200 :param trex_port: Port number to send packet from, must match a port in the TRex config file
201 :param l1_bps: L1 Throughput generated by TRex (mutually exclusive with percentage)
202 :param percentage: Percentage usage of the selected port bandwidth (mutually exlusive with l1_bps)
203 :param flow_id: Flow ID, required when saving latency statistics
204 :param flow_stats: True to measure flow statistics (latency and packet), False otherwise, might require software mode
205 :return: True if the stream is create, false otherwise
206 """
207 if (percentage is None and l1_bps is None) or (
208 percentage is not None and l1_bps is not None):
209 main.log.error(
210 "Either percentage or l1_bps must be provided when creating a stream")
211 return False
212 main.log.debug("Creating flow stream")
213 main.log.debug(
214 "port: %d, l1_bps: %s, percentage: %s, delay: %d, flow_id:%s, flow_stats: %s" % (
215 trex_port, str(l1_bps), str(percentage), delay, str(flow_id),
216 str(flow_stats)))
217 main.log.debug(pkt.summary())
218 if flow_stats:
219 traffic_stream = self.__create_latency_stats_stream(
220 pkt,
221 pg_id=flow_id,
222 isg=delay,
223 percentage=percentage,
224 l1_bps=l1_bps)
225 else:
226 traffic_stream = self.__create_background_stream(
227 pkt,
228 percentage=percentage,
229 l1_bps=l1_bps)
230 self.trex_client.add_streams(traffic_stream, ports=trex_port)
Daniele Morob8404e82022-02-25 00:17:28 +0100231 gen_traffic = self.gen_traffic_per_port.get(trex_port, 0)
232 gen_traffic += l1_bps
233 self.gen_traffic_per_port[trex_port] = gen_traffic
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200234 return True
235
Daniele Morof811f9f2021-09-21 19:07:52 +0200236 def startAndWaitTraffic(self, duration=10, ports=[]):
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200237 """
238 Start generating traffic and wait traffic to be send
Daniele Morof811f9f2021-09-21 19:07:52 +0200239
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200240 :param duration: Traffic generation duration
Daniele Morof811f9f2021-09-21 19:07:52 +0200241 :param ports: Ports IDs to monitor while traffic is active
242 :return: port statistics collected while traffic is active
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200243 """
244 if not self.trex_client:
245 main.log.error(
246 "Cannot start traffic, first connect the TRex client")
247 return False
Daniele Morob8404e82022-02-25 00:17:28 +0100248 # Reset stats from previous run
249 self.stats = None
250 main.step("Sending traffic for %d seconds" % duration)
251 self.trex_client.start(self.gen_traffic_per_port.keys(), mult="1",
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200252 duration=duration)
253 main.log.info("Waiting until all traffic is sent..")
Daniele Morob8404e82022-02-25 00:17:28 +0100254 result = self.__monitor_port_stats({p: self.gen_traffic_per_port.get(p, None) for p in ports})
255 self.trex_client.wait_on_traffic(ports=self.gen_traffic_per_port.keys(),
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200256 rx_delay_ms=100)
257 main.log.info("...traffic sent!")
258 # Reset sender port so we can run other tests with the same TRex client
Daniele Morob8404e82022-02-25 00:17:28 +0100259 self.gen_traffic_per_port = {}
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200260 main.log.info("Getting stats")
261 self.stats = self.trex_client.get_stats()
Daniele Morof811f9f2021-09-21 19:07:52 +0200262 return result
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200263
264 def getFlowStats(self, flow_id):
265 if self.stats is None:
266 main.log.error("No stats saved!")
267 return None
268 return TrexClientDriver.__get_flow_stats(flow_id, self.stats)
269
270 def logFlowStats(self, flow_id):
271 main.log.info("Statistics for flow {}: {}".format(
272 flow_id,
273 TrexClientDriver.__get_readable_flow_stats(
274 self.getFlowStats(flow_id))))
275
276 def getLatencyStats(self, flow_id):
277 if self.stats is None:
278 main.log.error("No stats saved!")
279 return None
280 return TrexClientDriver.__get_latency_stats(flow_id, self.stats)
281
282 def logLatencyStats(self, flow_id):
283 main.log.info("Latency statistics for flow {}: {}".format(
284 flow_id,
285 TrexClientDriver.__get_readable_latency_stats(
286 self.getLatencyStats(flow_id))))
287
288 def getPortStats(self, port_id):
289 if self.stats is None:
290 main.log.error("No stats saved!")
291 return None
292 return TrexClientDriver.__get_port_stats(port_id, self.stats)
293
294 def logPortStats(self, port_id):
295 if self.stats is None:
296 main.log.error("No stats saved!")
297 return None
298 main.log.info("Statistics for port {}: {}".format(
299 port_id, TrexClientDriver.__get_readable_port_stats(
300 self.stats.get(port_id))))
301
302 # From ptf/test/common/ptf_runner.py
303 def __set_up_trex_server(self, trex_daemon_client, trex_address,
304 trex_config,
305 force_restart):
306 try:
307 main.log.info("Pushing Trex config %s to the server" % trex_config)
308 if not trex_daemon_client.push_files(trex_config):
309 main.log.error("Unable to push %s to Trex server" % trex_config)
310 return False
311
312 if force_restart:
313 main.log.info("Restarting TRex")
314 trex_daemon_client.kill_all_trexes()
315 time.sleep(1)
316
317 if not trex_daemon_client.is_idle():
318 main.log.info("The Trex server process is running")
319 main.log.warn(
320 "A Trex server process is still running, "
321 + "use --force-restart to kill it if necessary."
322 )
323 return False
324
325 trex_config_file_on_server = TREX_FILES_DIR + os.path.basename(
326 trex_config)
327 trex_daemon_client.start_stateless(cfg=trex_config_file_on_server)
328 except ConnectionRefusedError:
329 main.log.error(
Jon Hall02424522021-12-10 12:44:31 -0800330 "Unable to connect to server %s.\nDid you start the Trex daemon?" % trex_address)
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200331 return False
332
333 return True
334
335 def __create_latency_stats_stream(self, pkt, pg_id,
336 name=None,
337 l1_bps=None,
338 percentage=None,
339 isg=0):
340 assert (percentage is None and l1_bps is not None) or (
341 percentage is not None and l1_bps is None)
342 return STLStream(
343 name=name,
344 packet=STLPktBuilder(pkt=pkt),
345 mode=STLTXCont(bps_L1=l1_bps, percentage=percentage),
346 isg=isg,
347 flow_stats=STLFlowLatencyStats(pg_id=pg_id)
348 )
349
350 def __create_background_stream(self, pkt, name=None, percentage=None,
351 l1_bps=None):
352 assert (percentage is None and l1_bps is not None) or (
353 percentage is not None and l1_bps is None)
354 return STLStream(
355 name=name,
356 packet=STLPktBuilder(pkt=pkt),
357 mode=STLTXCont(bps_L1=l1_bps, percentage=percentage)
358 )
359
360 # Multiplier for data rates
361 K = 1000
362 M = 1000 * K
363 G = 1000 * M
364
Daniele Morob8404e82022-02-25 00:17:28 +0100365 def __monitor_port_stats(self, target_tx_per_port, num_samples=4,
366 ramp_up_timeout=5, time_interval=1, min_tx_bps_margin=0.95):
Daniele Morof811f9f2021-09-21 19:07:52 +0200367 """
Daniele Morob8404e82022-02-25 00:17:28 +0100368 List some port stats continuously while traffic is active and verify that
369 the generated amount traffic is the expected one
Daniele Morof811f9f2021-09-21 19:07:52 +0200370
Daniele Morob8404e82022-02-25 00:17:28 +0100371 :param target_tx_per_port: Traffic to be generated per port
Daniele Morof811f9f2021-09-21 19:07:52 +0200372 :param time_interval: Interval between read
Daniele Morob8404e82022-02-25 00:17:28 +0100373 :param num_samples: Number of samples of statistics from each monitored ports
374 :param ramp_up_timeout: how many seconds to wait before TRex can reach the target TX rate
Yi Tseng64a60a72021-11-03 14:26:33 -0700375 :return: Statistics read while traffic is active, or empty result if no
Daniele Morob8404e82022-02-25 00:17:28 +0100376 target_tx_per_port provided.
Daniele Morof811f9f2021-09-21 19:07:52 +0200377 """
Daniele Morob8404e82022-02-25 00:17:28 +0100378
379 ports = target_tx_per_port.keys()
380 local_gen_traffic_per_port = copy.deepcopy(target_tx_per_port)
Daniele Morof811f9f2021-09-21 19:07:52 +0200381 results = {
382 port_id: {"rx_bps": [], "tx_bps": [], "rx_pps": [], "tx_pps": []}
383 for port_id in ports
384 }
385 results["duration"] = []
386
Daniele Morob8404e82022-02-25 00:17:28 +0100387 if len(ports) == 0:
Yi Tseng64a60a72021-11-03 14:26:33 -0700388 return results
389
Daniele Morob8404e82022-02-25 00:17:28 +0100390 start_time = time.time()
Daniele Morof811f9f2021-09-21 19:07:52 +0200391 prev = {
392 port_id: {
393 "opackets": 0,
394 "ipackets": 0,
395 "obytes": 0,
396 "ibytes": 0,
Daniele Morob8404e82022-02-25 00:17:28 +0100397 "time": start_time,
Daniele Morof811f9f2021-09-21 19:07:52 +0200398 }
399 for port_id in ports
400 }
401
Daniele Morob8404e82022-02-25 00:17:28 +0100402 time.sleep(time_interval)
Daniele Morof811f9f2021-09-21 19:07:52 +0200403 while self.trex_client.is_traffic_active():
404 stats = self.trex_client.get_stats(ports=ports)
Daniele Morob8404e82022-02-25 00:17:28 +0100405 sample_time = time.time()
406 elapsed = sample_time - start_time
Daniele Morof811f9f2021-09-21 19:07:52 +0200407 if not stats:
408 break
409
410 main.log.debug(
Daniele Morob8404e82022-02-25 00:17:28 +0100411 "\nTRAFFIC RUNNING {:.2f} SEC".format(elapsed))
Daniele Morof811f9f2021-09-21 19:07:52 +0200412 main.log.debug(
413 "{:^4} | {:<10} | {:<10} | {:<10} | {:<10} |".format(
414 "Port", "RX bps", "TX bps", "RX pps", "TX pps"
415 )
416 )
417 main.log.debug(
418 "----------------------------------------------------------")
419
Daniele Morob8404e82022-02-25 00:17:28 +0100420 for (tx_port, target_tx_rate) in local_gen_traffic_per_port.items():
421 opackets = stats[tx_port]["opackets"]
422 ipackets = stats[tx_port]["ipackets"]
423 obytes = stats[tx_port]["obytes"]
424 ibytes = stats[tx_port]["ibytes"]
425 time_diff = sample_time - prev[tx_port]["time"]
Daniele Morof811f9f2021-09-21 19:07:52 +0200426
Daniele Morob8404e82022-02-25 00:17:28 +0100427 rx_bps = 8 * (ibytes - prev[tx_port]["ibytes"]) / time_diff
428 tx_bps = 8 * (obytes - prev[tx_port]["obytes"]) / time_diff
429 rx_pps = ipackets - prev[tx_port]["ipackets"] / time_diff
430 tx_pps = opackets - prev[tx_port]["opackets"] / time_diff
Daniele Morof811f9f2021-09-21 19:07:52 +0200431
432 main.log.debug(
433 "{:^4} | {:<10} | {:<10} | {:<10} | {:<10} |".format(
Daniele Morob8404e82022-02-25 00:17:28 +0100434 tx_port,
Daniele Morof811f9f2021-09-21 19:07:52 +0200435 TrexClientDriver.__to_readable(rx_bps, "bps"),
436 TrexClientDriver.__to_readable(tx_bps, "bps"),
437 TrexClientDriver.__to_readable(rx_pps, "pps"),
438 TrexClientDriver.__to_readable(tx_pps, "pps"),
439 )
440 )
441
Daniele Morob8404e82022-02-25 00:17:28 +0100442 results["duration"].append(sample_time - start_time)
443 results[tx_port]["rx_bps"].append(rx_bps)
444 results[tx_port]["tx_bps"].append(tx_bps)
445 results[tx_port]["rx_pps"].append(rx_pps)
446 results[tx_port]["tx_pps"].append(tx_pps)
Daniele Morof811f9f2021-09-21 19:07:52 +0200447
Daniele Morob8404e82022-02-25 00:17:28 +0100448 prev[tx_port]["opackets"] = opackets
449 prev[tx_port]["ipackets"] = ipackets
450 prev[tx_port]["obytes"] = obytes
451 prev[tx_port]["ibytes"] = ibytes
452 prev[tx_port]["time"] = sample_time
453
454 if target_tx_rate is not None:
455 if tx_bps < (target_tx_rate * min_tx_bps_margin):
456 if elapsed > ramp_up_timeout:
457 self.trex_client.stop(ports=ports)
458 utilities.assert_equal(
459 expect=True, actual=False,
460 onpass="Should never reach this",
461 onfail="TX port ({}) did not reach or sustain min sending rate ({})".format(
462 tx_port, target_tx_rate)
463 )
464 return {}
465 else:
466 results[tx_port]["rx_bps"].pop()
467 results[tx_port]["tx_bps"].pop()
468 results[tx_port]["rx_pps"].pop()
469 results[tx_port]["tx_pps"].pop()
470
471 if len(results[tx_port]["tx_bps"]) == num_samples:
472 # Stop monitoring ports for which we have enough samples
473 del local_gen_traffic_per_port[tx_port]
474
475 if len(local_gen_traffic_per_port) == 0:
476 # Enough samples for all ports
477 utilities.assert_equal(
478 expect=True, actual=True,
479 onpass="Enough samples have been generated",
480 onfail="Should never reach this"
481 )
482 return results
Daniele Morof811f9f2021-09-21 19:07:52 +0200483
484 time.sleep(time_interval)
485 main.log.debug("")
486
Daniele Morob8404e82022-02-25 00:17:28 +0100487 utilities.assert_equal(
488 expect=True, actual=True,
489 onpass="Traffic sent correctly",
490 onfail="Should never reach this"
491 )
Daniele Morof811f9f2021-09-21 19:07:52 +0200492 return results
493
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200494 @staticmethod
495 def __to_readable(src, unit="bps"):
496 """
497 Convert number to human readable string.
498 For example: 1,000,000 bps to 1Mbps. 1,000 bytes to 1KB
499
500 :parameters:
501 src : int
502 the original data
503 unit : str
504 the unit ('bps', 'pps', or 'bytes')
505 :returns:
506 A human readable string
507 """
508 if src < 1000:
509 return "{:.1f} {}".format(src, unit)
510 elif src < 1000000:
511 return "{:.1f} K{}".format(src / 1000, unit)
512 elif src < 1000000000:
513 return "{:.1f} M{}".format(src / 1000000, unit)
514 else:
515 return "{:.1f} G{}".format(src / 1000000000, unit)
516
517 @staticmethod
518 def __get_readable_port_stats(port_stats):
519 opackets = port_stats.get("opackets", 0)
520 ipackets = port_stats.get("ipackets", 0)
521 obytes = port_stats.get("obytes", 0)
522 ibytes = port_stats.get("ibytes", 0)
523 oerrors = port_stats.get("oerrors", 0)
524 ierrors = port_stats.get("ierrors", 0)
525 tx_bps = port_stats.get("tx_bps", 0)
526 tx_pps = port_stats.get("tx_pps", 0)
527 tx_bps_L1 = port_stats.get("tx_bps_L1", 0)
528 tx_util = port_stats.get("tx_util", 0)
529 rx_bps = port_stats.get("rx_bps", 0)
530 rx_pps = port_stats.get("rx_pps", 0)
531 rx_bps_L1 = port_stats.get("rx_bps_L1", 0)
532 rx_util = port_stats.get("rx_util", 0)
533 return """
534 Output packets: {}
535 Input packets: {}
536 Output bytes: {} ({})
537 Input bytes: {} ({})
538 Output errors: {}
539 Input errors: {}
540 TX bps: {} ({})
541 TX pps: {} ({})
542 L1 TX bps: {} ({})
543 TX util: {}
544 RX bps: {} ({})
545 RX pps: {} ({})
546 L1 RX bps: {} ({})
547 RX util: {}""".format(
548 opackets,
549 ipackets,
550 obytes,
551 TrexClientDriver.__to_readable(obytes, "Bytes"),
552 ibytes,
553 TrexClientDriver.__to_readable(ibytes, "Bytes"),
554 oerrors,
555 ierrors,
556 tx_bps,
557 TrexClientDriver.__to_readable(tx_bps),
558 tx_pps,
559 TrexClientDriver.__to_readable(tx_pps, "pps"),
560 tx_bps_L1,
561 TrexClientDriver.__to_readable(tx_bps_L1),
562 tx_util,
563 rx_bps,
564 TrexClientDriver.__to_readable(rx_bps),
565 rx_pps,
566 TrexClientDriver.__to_readable(rx_pps, "pps"),
567 rx_bps_L1,
568 TrexClientDriver.__to_readable(rx_bps_L1),
569 rx_util,
570 )
571
572 @staticmethod
573 def __get_port_stats(port, stats):
574 """
575 :param port: int
576 :param stats:
577 :return:
578 """
579 port_stats = stats.get(port)
580 return PortStats(
581 tx_packets=port_stats.get("opackets", 0),
582 rx_packets=port_stats.get("ipackets", 0),
583 tx_bytes=port_stats.get("obytes", 0),
584 rx_bytes=port_stats.get("ibytes", 0),
585 tx_errors=port_stats.get("oerrors", 0),
586 rx_errors=port_stats.get("ierrors", 0),
587 tx_bps=port_stats.get("tx_bps", 0),
588 tx_pps=port_stats.get("tx_pps", 0),
589 tx_bps_L1=port_stats.get("tx_bps_L1", 0),
590 tx_util=port_stats.get("tx_util", 0),
591 rx_bps=port_stats.get("rx_bps", 0),
592 rx_pps=port_stats.get("rx_pps", 0),
593 rx_bps_L1=port_stats.get("rx_bps_L1", 0),
594 rx_util=port_stats.get("rx_util", 0),
595 )
596
597 @staticmethod
598 def __get_latency_stats(pg_id, stats):
599 """
600 :param pg_id: int
601 :param stats:
602 :return:
603 """
604
605 lat_stats = stats["latency"].get(pg_id)
606 lat = lat_stats["latency"]
607 # Estimate latency percentiles from the histogram.
608 l = list(lat["histogram"].keys())
609 l.sort()
610 all_latencies = []
611 for sample in l:
612 range_start = sample
613 if range_start == 0:
614 range_end = 10
615 else:
616 range_end = range_start + pow(10, (len(str(range_start)) - 1))
617 val = lat["histogram"][sample]
618 # Assume whole the bucket experienced the range_end latency.
619 all_latencies += [range_end] * val
620 q = [50, 75, 90, 99, 99.9, 99.99, 99.999]
Daniele Moro49460072022-02-03 14:30:00 +0100621 # Prevent exception if we have no latency histogram
622 percentiles = np.percentile(all_latencies, q) if len(all_latencies) > 0 else [sys.maxint] * len(q)
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200623
624 ret = LatencyStats(
625 pg_id=pg_id,
626 jitter=lat["jitter"],
627 average=lat["average"],
628 total_max=lat["total_max"],
629 total_min=lat["total_min"],
630 last_max=lat["last_max"],
631 histogram=lat["histogram"],
632 dropped=lat_stats["err_cntrs"]["dropped"],
633 out_of_order=lat_stats["err_cntrs"]["out_of_order"],
634 duplicate=lat_stats["err_cntrs"]["dup"],
635 seq_too_high=lat_stats["err_cntrs"]["seq_too_high"],
636 seq_too_low=lat_stats["err_cntrs"]["seq_too_low"],
637 percentile_50=percentiles[0],
638 percentile_75=percentiles[1],
639 percentile_90=percentiles[2],
640 percentile_99=percentiles[3],
641 percentile_99_9=percentiles[4],
642 percentile_99_99=percentiles[5],
643 percentile_99_999=percentiles[6],
644 )
645 return ret
646
647 @staticmethod
648 def __get_readable_latency_stats(stats):
649 """
650 :param stats: LatencyStats
651 :return:
652 """
653 histogram = ""
654 # need to listify in order to be able to sort them.
655 l = list(stats.histogram.keys())
656 l.sort()
657 for sample in l:
658 range_start = sample
659 if range_start == 0:
660 range_end = 10
661 else:
662 range_end = range_start + pow(10, (len(str(range_start)) - 1))
663 val = stats.histogram[sample]
664 histogram = (
665 histogram
666 + "\n Packets with latency between {0:>5} us and {1:>5} us: {2:>10}".format(
667 range_start, range_end, val
668 )
669 )
670
671 return """
672 Latency info for pg_id {}
673 Dropped packets: {}
674 Out-of-order packets: {}
675 Sequence too high packets: {}
676 Sequence too low packets: {}
677 Maximum latency: {} us
678 Minimum latency: {} us
679 Maximum latency in last sampling period: {} us
680 Average latency: {} us
681 50th percentile latency: {} us
682 75th percentile latency: {} us
683 90th percentile latency: {} us
684 99th percentile latency: {} us
685 99.9th percentile latency: {} us
686 99.99th percentile latency: {} us
687 99.999th percentile latency: {} us
688 Jitter: {} us
689 Latency distribution histogram: {}
690 """.format(stats.pg_id, stats.dropped, stats.out_of_order,
691 stats.seq_too_high, stats.seq_too_low, stats.total_max,
692 stats.total_min, stats.last_max, stats.average,
693 stats.percentile_50, stats.percentile_75,
694 stats.percentile_90,
695 stats.percentile_99, stats.percentile_99_9,
696 stats.percentile_99_99,
697 stats.percentile_99_999, stats.jitter, histogram)
698
699 @staticmethod
700 def __get_flow_stats(pg_id, stats):
701 """
702 :param pg_id: int
703 :param stats:
704 :return:
705 """
706 FlowStats = collections.namedtuple(
707 "FlowStats",
708 ["pg_id", "tx_packets", "rx_packets", "tx_bytes", "rx_bytes", ],
709 )
710 flow_stats = stats["flow_stats"].get(pg_id)
711 ret = FlowStats(
712 pg_id=pg_id,
713 tx_packets=flow_stats["tx_pkts"]["total"],
714 rx_packets=flow_stats["rx_pkts"]["total"],
715 tx_bytes=flow_stats["tx_bytes"]["total"],
716 rx_bytes=flow_stats["rx_bytes"]["total"],
717 )
718 return ret
719
720 @staticmethod
721 def __get_readable_flow_stats(stats):
722 """
723 :param stats: FlowStats
724 :return:
725 """
726 return """Flow info for pg_id {}
727 TX packets: {}
728 RX packets: {}
729 TX bytes: {}
730 RX bytes: {}""".format(stats.pg_id, stats.tx_packets,
731 stats.rx_packets, stats.tx_bytes,
732 stats.rx_bytes)