blob: de65f3a30b795ef0bdacee31fcc0c161ec56e127 [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 Moro732055a2021-09-28 15:28:46 +020011import sys
12import importlib
Daniele Moro4a5a91f2021-09-07 17:24:39 +020013import collections
14import numpy as np
15
16from drivers.common.api.controllerdriver import Controller
Daniele Moro4a5a91f2021-09-07 17:24:39 +020017
18from socket import error as ConnectionRefusedError
19from distutils.util import strtobool
20
21TREX_FILES_DIR = "/tmp/trex_files/"
22
23LatencyStats = collections.namedtuple(
24 "LatencyStats",
25 [
26 "pg_id",
27 "jitter",
28 "average",
29 "total_max",
30 "total_min",
31 "last_max",
32 "histogram",
33 "dropped",
34 "out_of_order",
35 "duplicate",
36 "seq_too_high",
37 "seq_too_low",
38 "percentile_50",
39 "percentile_75",
40 "percentile_90",
41 "percentile_99",
42 "percentile_99_9",
43 "percentile_99_99",
44 "percentile_99_999",
45 ],
46)
47
48PortStats = collections.namedtuple(
49 "PortStats",
50 [
51 "tx_packets",
52 "rx_packets",
53 "tx_bytes",
54 "rx_bytes",
55 "tx_errors",
56 "rx_errors",
57 "tx_bps",
58 "tx_pps",
59 "tx_bps_L1",
60 "tx_util",
61 "rx_bps",
62 "rx_pps",
63 "rx_bps_L1",
64 "rx_util",
65 ],
66)
67
68FlowStats = collections.namedtuple(
69 "FlowStats",
70 [
71 "pg_id",
72 "tx_packets",
73 "rx_packets",
74 "tx_bytes",
75 "rx_bytes",
76 ],
77)
78
79
80class TrexClientDriver(Controller):
81 """
82 Implements a Trex Client Driver
83 """
84
85 def __init__(self):
86 self.trex_address = "localhost"
87 self.trex_config = None # Relative path in dependencies of the test using this driver
88 self.force_restart = True
89 self.sofware_mode = False
90 self.setup_successful = False
91 self.stats = None
92 self.trex_client = None
93 self.trex_daemon_client = None
Daniele Moro732055a2021-09-28 15:28:46 +020094 self.trex_library_python_path = None
Daniele Moro4a5a91f2021-09-07 17:24:39 +020095 super(TrexClientDriver, self).__init__()
96
97 def connect(self, **connectargs):
Daniele Moro732055a2021-09-28 15:28:46 +020098 global STLClient, STLStreamDstMAC_PKT, CTRexClient, STLPktBuilder, \
99 STLFlowLatencyStats, STLStream, STLTXCont
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200100 try:
101 for key in connectargs:
102 vars(self)[key] = connectargs[key]
103 for key in self.options:
104 if key == "trex_address":
105 self.trex_address = self.options[key]
106 elif key == "trex_config":
107 self.trex_config = self.options[key]
108 elif key == "force_restart":
109 self.force_restart = bool(strtobool(self.options[key]))
110 elif key == "software_mode":
111 self.software_mode = bool(strtobool(self.options[key]))
Daniele Moro732055a2021-09-28 15:28:46 +0200112 elif key == "trex_library_python_path":
113 self.trex_library_python_path = self.options[key]
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200114 self.name = self.options["name"]
Daniele Moro732055a2021-09-28 15:28:46 +0200115 if self.trex_library_python_path is not None:
116 sys.path.append(self.trex_library_python_path)
117 # Import after appending the TRex library Python path
118 STLClient = getattr(importlib.import_module("trex.stl.api"), "STLClient")
119 STLStreamDstMAC_PKT = getattr(importlib.import_module("trex.stl.api"), "STLStreamDstMAC_PKT")
120 CTRexClient = getattr(importlib.import_module("trex_stf_lib.trex_client"), "CTRexClient")
121 STLFlowLatencyStats = getattr(importlib.import_module("trex_stl_lib.api"), "STLFlowLatencyStats")
122 STLPktBuilder = getattr(importlib.import_module("trex_stl_lib.api"), "STLPktBuilder")
123 STLStream = getattr(importlib.import_module("trex_stl_lib.api"), "STLStream")
124 STLTXCont = getattr(importlib.import_module("trex_stl_lib.api"), "STLTXCont")
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200125 except Exception as inst:
126 main.log.error("Uncaught exception: " + str(inst))
127 main.cleanAndExit()
128 return super(TrexClientDriver, self).connect()
129
130 def disconnect(self):
131 """
132 Called when Test is complete
133 """
134 self.disconnectTrexClient()
135 self.stopTrexServer()
136 return main.TRUE
137
138 def setupTrex(self, pathToTrexConfig):
139 """
140 Setup TRex server passing the TRex configuration.
141 :return: True if setup successful, False otherwise
142 """
143 main.log.debug(self.name + ": Setting up TRex server")
144 if self.software_mode:
145 trex_args = "--software --no-hw-flow-stat"
146 else:
147 trex_args = None
148 self.trex_daemon_client = CTRexClient(self.trex_address,
149 trex_args=trex_args)
150 success = self.__set_up_trex_server(
151 self.trex_daemon_client, self.trex_address,
Yi Tsengdda7e322021-09-20 14:21:20 -0700152 os.path.join(pathToTrexConfig, self.trex_config),
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200153 self.force_restart
154 )
155 if not success:
156 main.log.error("Failed to set up TRex daemon!")
157 return False
158 self.setup_successful = True
159 return True
160
161 def connectTrexClient(self):
162 if not self.setup_successful:
163 main.log.error("Cannot connect TRex Client, first setup TRex")
164 return False
165 main.log.info("Connecting TRex Client")
166 self.trex_client = STLClient(server=self.trex_address)
167 self.trex_client.connect()
168 self.trex_client.acquire()
169 self.trex_client.reset() # Resets configs from all ports
170 self.trex_client.clear_stats() # Clear status from all ports
171 # Put all ports to promiscuous mode, otherwise they will drop all
172 # incoming packets if the destination mac is not the port mac address.
173 self.trex_client.set_port_attr(self.trex_client.get_all_ports(),
174 promiscuous=True)
175 # Reset the used sender ports
176 self.all_sender_port = set()
177 self.stats = None
178 return True
179
180 def disconnectTrexClient(self):
181 # Teardown TREX Client
182 if self.trex_client is not None:
183 main.log.info("Tearing down STLClient...")
184 self.trex_client.stop()
185 self.trex_client.release()
186 self.trex_client.disconnect()
187 self.trex_client = None
188 # Do not reset stats
189
190 def stopTrexServer(self):
191 if self.trex_daemon_client is not None:
192 self.trex_daemon_client.stop_trex()
193 self.trex_daemon_client = None
194
195 def addStream(self, pkt, trex_port, l1_bps=None, percentage=None,
196 delay=0, flow_id=None, flow_stats=False):
197 """
198 :param pkt: Scapy packet, TRex will send copy of this packet
199 :param trex_port: Port number to send packet from, must match a port in the TRex config file
200 :param l1_bps: L1 Throughput generated by TRex (mutually exclusive with percentage)
201 :param percentage: Percentage usage of the selected port bandwidth (mutually exlusive with l1_bps)
202 :param flow_id: Flow ID, required when saving latency statistics
203 :param flow_stats: True to measure flow statistics (latency and packet), False otherwise, might require software mode
204 :return: True if the stream is create, false otherwise
205 """
206 if (percentage is None and l1_bps is None) or (
207 percentage is not None and l1_bps is not None):
208 main.log.error(
209 "Either percentage or l1_bps must be provided when creating a stream")
210 return False
211 main.log.debug("Creating flow stream")
212 main.log.debug(
213 "port: %d, l1_bps: %s, percentage: %s, delay: %d, flow_id:%s, flow_stats: %s" % (
214 trex_port, str(l1_bps), str(percentage), delay, str(flow_id),
215 str(flow_stats)))
216 main.log.debug(pkt.summary())
217 if flow_stats:
218 traffic_stream = self.__create_latency_stats_stream(
219 pkt,
220 pg_id=flow_id,
221 isg=delay,
222 percentage=percentage,
223 l1_bps=l1_bps)
224 else:
225 traffic_stream = self.__create_background_stream(
226 pkt,
227 percentage=percentage,
228 l1_bps=l1_bps)
229 self.trex_client.add_streams(traffic_stream, ports=trex_port)
230 self.all_sender_port.add(trex_port)
231 return True
232
Daniele Morof811f9f2021-09-21 19:07:52 +0200233 def startAndWaitTraffic(self, duration=10, ports=[]):
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200234 """
235 Start generating traffic and wait traffic to be send
Daniele Morof811f9f2021-09-21 19:07:52 +0200236
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200237 :param duration: Traffic generation duration
Daniele Morof811f9f2021-09-21 19:07:52 +0200238 :param ports: Ports IDs to monitor while traffic is active
239 :return: port statistics collected while traffic is active
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200240 """
241 if not self.trex_client:
242 main.log.error(
243 "Cannot start traffic, first connect the TRex client")
244 return False
245 main.log.info("Start sending traffic for %d seconds" % duration)
246 self.trex_client.start(list(self.all_sender_port), mult="1",
247 duration=duration)
248 main.log.info("Waiting until all traffic is sent..")
Daniele Morof811f9f2021-09-21 19:07:52 +0200249 result = self.__monitor_port_stats(ports)
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200250 self.trex_client.wait_on_traffic(ports=list(self.all_sender_port),
251 rx_delay_ms=100)
252 main.log.info("...traffic sent!")
253 # Reset sender port so we can run other tests with the same TRex client
254 self.all_sender_port = set()
255 main.log.info("Getting stats")
256 self.stats = self.trex_client.get_stats()
Daniele Morof811f9f2021-09-21 19:07:52 +0200257 return result
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200258
259 def getFlowStats(self, flow_id):
260 if self.stats is None:
261 main.log.error("No stats saved!")
262 return None
263 return TrexClientDriver.__get_flow_stats(flow_id, self.stats)
264
265 def logFlowStats(self, flow_id):
266 main.log.info("Statistics for flow {}: {}".format(
267 flow_id,
268 TrexClientDriver.__get_readable_flow_stats(
269 self.getFlowStats(flow_id))))
270
271 def getLatencyStats(self, flow_id):
272 if self.stats is None:
273 main.log.error("No stats saved!")
274 return None
275 return TrexClientDriver.__get_latency_stats(flow_id, self.stats)
276
277 def logLatencyStats(self, flow_id):
278 main.log.info("Latency statistics for flow {}: {}".format(
279 flow_id,
280 TrexClientDriver.__get_readable_latency_stats(
281 self.getLatencyStats(flow_id))))
282
283 def getPortStats(self, port_id):
284 if self.stats is None:
285 main.log.error("No stats saved!")
286 return None
287 return TrexClientDriver.__get_port_stats(port_id, self.stats)
288
289 def logPortStats(self, port_id):
290 if self.stats is None:
291 main.log.error("No stats saved!")
292 return None
293 main.log.info("Statistics for port {}: {}".format(
294 port_id, TrexClientDriver.__get_readable_port_stats(
295 self.stats.get(port_id))))
296
297 # From ptf/test/common/ptf_runner.py
298 def __set_up_trex_server(self, trex_daemon_client, trex_address,
299 trex_config,
300 force_restart):
301 try:
302 main.log.info("Pushing Trex config %s to the server" % trex_config)
303 if not trex_daemon_client.push_files(trex_config):
304 main.log.error("Unable to push %s to Trex server" % trex_config)
305 return False
306
307 if force_restart:
308 main.log.info("Restarting TRex")
309 trex_daemon_client.kill_all_trexes()
310 time.sleep(1)
311
312 if not trex_daemon_client.is_idle():
313 main.log.info("The Trex server process is running")
314 main.log.warn(
315 "A Trex server process is still running, "
316 + "use --force-restart to kill it if necessary."
317 )
318 return False
319
320 trex_config_file_on_server = TREX_FILES_DIR + os.path.basename(
321 trex_config)
322 trex_daemon_client.start_stateless(cfg=trex_config_file_on_server)
323 except ConnectionRefusedError:
324 main.log.error(
Jon Hall02424522021-12-10 12:44:31 -0800325 "Unable to connect to server %s.\nDid you start the Trex daemon?" % trex_address)
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200326 return False
327
328 return True
329
330 def __create_latency_stats_stream(self, pkt, pg_id,
331 name=None,
332 l1_bps=None,
333 percentage=None,
334 isg=0):
335 assert (percentage is None and l1_bps is not None) or (
336 percentage is not None and l1_bps is None)
337 return STLStream(
338 name=name,
339 packet=STLPktBuilder(pkt=pkt),
340 mode=STLTXCont(bps_L1=l1_bps, percentage=percentage),
341 isg=isg,
342 flow_stats=STLFlowLatencyStats(pg_id=pg_id)
343 )
344
345 def __create_background_stream(self, pkt, name=None, percentage=None,
346 l1_bps=None):
347 assert (percentage is None and l1_bps is not None) or (
348 percentage is not None and l1_bps is None)
349 return STLStream(
350 name=name,
351 packet=STLPktBuilder(pkt=pkt),
352 mode=STLTXCont(bps_L1=l1_bps, percentage=percentage)
353 )
354
355 # Multiplier for data rates
356 K = 1000
357 M = 1000 * K
358 G = 1000 * M
359
Daniele Morof811f9f2021-09-21 19:07:52 +0200360 def __monitor_port_stats(self, ports, time_interval=1):
361 """
362 List some port stats continuously while traffic is active
363
364 :param ports: List of ports ids to monitor
365 :param time_interval: Interval between read
Yi Tseng64a60a72021-11-03 14:26:33 -0700366 :return: Statistics read while traffic is active, or empty result if no
367 ports provided.
Daniele Morof811f9f2021-09-21 19:07:52 +0200368 """
Daniele Morof811f9f2021-09-21 19:07:52 +0200369 results = {
370 port_id: {"rx_bps": [], "tx_bps": [], "rx_pps": [], "tx_pps": []}
371 for port_id in ports
372 }
373 results["duration"] = []
374
Yi Tseng64a60a72021-11-03 14:26:33 -0700375 if not ports:
376 return results
377
Daniele Morof811f9f2021-09-21 19:07:52 +0200378 prev = {
379 port_id: {
380 "opackets": 0,
381 "ipackets": 0,
382 "obytes": 0,
383 "ibytes": 0,
384 "time": time.time(),
385 }
386 for port_id in ports
387 }
388
389 s_time = time.time()
390 while self.trex_client.is_traffic_active():
391 stats = self.trex_client.get_stats(ports=ports)
392 if not stats:
393 break
394
395 main.log.debug(
396 "\nTRAFFIC RUNNING {:.2f} SEC".format(time.time() - s_time))
397 main.log.debug(
398 "{:^4} | {:<10} | {:<10} | {:<10} | {:<10} |".format(
399 "Port", "RX bps", "TX bps", "RX pps", "TX pps"
400 )
401 )
402 main.log.debug(
403 "----------------------------------------------------------")
404
405 for port in ports:
406 opackets = stats[port]["opackets"]
407 ipackets = stats[port]["ipackets"]
408 obytes = stats[port]["obytes"]
409 ibytes = stats[port]["ibytes"]
410 time_diff = time.time() - prev[port]["time"]
411
412 rx_bps = 8 * (ibytes - prev[port]["ibytes"]) / time_diff
413 tx_bps = 8 * (obytes - prev[port]["obytes"]) / time_diff
414 rx_pps = ipackets - prev[port]["ipackets"] / time_diff
415 tx_pps = opackets - prev[port]["opackets"] / time_diff
416
417 main.log.debug(
418 "{:^4} | {:<10} | {:<10} | {:<10} | {:<10} |".format(
419 port,
420 TrexClientDriver.__to_readable(rx_bps, "bps"),
421 TrexClientDriver.__to_readable(tx_bps, "bps"),
422 TrexClientDriver.__to_readable(rx_pps, "pps"),
423 TrexClientDriver.__to_readable(tx_pps, "pps"),
424 )
425 )
426
427 results["duration"].append(time.time() - s_time)
428 results[port]["rx_bps"].append(rx_bps)
429 results[port]["tx_bps"].append(tx_bps)
430 results[port]["rx_pps"].append(rx_pps)
431 results[port]["tx_pps"].append(tx_pps)
432
433 prev[port]["opackets"] = opackets
434 prev[port]["ipackets"] = ipackets
435 prev[port]["obytes"] = obytes
436 prev[port]["ibytes"] = ibytes
437 prev[port]["time"] = time.time()
438
439 time.sleep(time_interval)
440 main.log.debug("")
441
442 return results
443
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200444 @staticmethod
445 def __to_readable(src, unit="bps"):
446 """
447 Convert number to human readable string.
448 For example: 1,000,000 bps to 1Mbps. 1,000 bytes to 1KB
449
450 :parameters:
451 src : int
452 the original data
453 unit : str
454 the unit ('bps', 'pps', or 'bytes')
455 :returns:
456 A human readable string
457 """
458 if src < 1000:
459 return "{:.1f} {}".format(src, unit)
460 elif src < 1000000:
461 return "{:.1f} K{}".format(src / 1000, unit)
462 elif src < 1000000000:
463 return "{:.1f} M{}".format(src / 1000000, unit)
464 else:
465 return "{:.1f} G{}".format(src / 1000000000, unit)
466
467 @staticmethod
468 def __get_readable_port_stats(port_stats):
469 opackets = port_stats.get("opackets", 0)
470 ipackets = port_stats.get("ipackets", 0)
471 obytes = port_stats.get("obytes", 0)
472 ibytes = port_stats.get("ibytes", 0)
473 oerrors = port_stats.get("oerrors", 0)
474 ierrors = port_stats.get("ierrors", 0)
475 tx_bps = port_stats.get("tx_bps", 0)
476 tx_pps = port_stats.get("tx_pps", 0)
477 tx_bps_L1 = port_stats.get("tx_bps_L1", 0)
478 tx_util = port_stats.get("tx_util", 0)
479 rx_bps = port_stats.get("rx_bps", 0)
480 rx_pps = port_stats.get("rx_pps", 0)
481 rx_bps_L1 = port_stats.get("rx_bps_L1", 0)
482 rx_util = port_stats.get("rx_util", 0)
483 return """
484 Output packets: {}
485 Input packets: {}
486 Output bytes: {} ({})
487 Input bytes: {} ({})
488 Output errors: {}
489 Input errors: {}
490 TX bps: {} ({})
491 TX pps: {} ({})
492 L1 TX bps: {} ({})
493 TX util: {}
494 RX bps: {} ({})
495 RX pps: {} ({})
496 L1 RX bps: {} ({})
497 RX util: {}""".format(
498 opackets,
499 ipackets,
500 obytes,
501 TrexClientDriver.__to_readable(obytes, "Bytes"),
502 ibytes,
503 TrexClientDriver.__to_readable(ibytes, "Bytes"),
504 oerrors,
505 ierrors,
506 tx_bps,
507 TrexClientDriver.__to_readable(tx_bps),
508 tx_pps,
509 TrexClientDriver.__to_readable(tx_pps, "pps"),
510 tx_bps_L1,
511 TrexClientDriver.__to_readable(tx_bps_L1),
512 tx_util,
513 rx_bps,
514 TrexClientDriver.__to_readable(rx_bps),
515 rx_pps,
516 TrexClientDriver.__to_readable(rx_pps, "pps"),
517 rx_bps_L1,
518 TrexClientDriver.__to_readable(rx_bps_L1),
519 rx_util,
520 )
521
522 @staticmethod
523 def __get_port_stats(port, stats):
524 """
525 :param port: int
526 :param stats:
527 :return:
528 """
529 port_stats = stats.get(port)
530 return PortStats(
531 tx_packets=port_stats.get("opackets", 0),
532 rx_packets=port_stats.get("ipackets", 0),
533 tx_bytes=port_stats.get("obytes", 0),
534 rx_bytes=port_stats.get("ibytes", 0),
535 tx_errors=port_stats.get("oerrors", 0),
536 rx_errors=port_stats.get("ierrors", 0),
537 tx_bps=port_stats.get("tx_bps", 0),
538 tx_pps=port_stats.get("tx_pps", 0),
539 tx_bps_L1=port_stats.get("tx_bps_L1", 0),
540 tx_util=port_stats.get("tx_util", 0),
541 rx_bps=port_stats.get("rx_bps", 0),
542 rx_pps=port_stats.get("rx_pps", 0),
543 rx_bps_L1=port_stats.get("rx_bps_L1", 0),
544 rx_util=port_stats.get("rx_util", 0),
545 )
546
547 @staticmethod
548 def __get_latency_stats(pg_id, stats):
549 """
550 :param pg_id: int
551 :param stats:
552 :return:
553 """
554
555 lat_stats = stats["latency"].get(pg_id)
556 lat = lat_stats["latency"]
557 # Estimate latency percentiles from the histogram.
558 l = list(lat["histogram"].keys())
559 l.sort()
560 all_latencies = []
561 for sample in l:
562 range_start = sample
563 if range_start == 0:
564 range_end = 10
565 else:
566 range_end = range_start + pow(10, (len(str(range_start)) - 1))
567 val = lat["histogram"][sample]
568 # Assume whole the bucket experienced the range_end latency.
569 all_latencies += [range_end] * val
570 q = [50, 75, 90, 99, 99.9, 99.99, 99.999]
Daniele Moro49460072022-02-03 14:30:00 +0100571 # Prevent exception if we have no latency histogram
572 percentiles = np.percentile(all_latencies, q) if len(all_latencies) > 0 else [sys.maxint] * len(q)
Daniele Moro4a5a91f2021-09-07 17:24:39 +0200573
574 ret = LatencyStats(
575 pg_id=pg_id,
576 jitter=lat["jitter"],
577 average=lat["average"],
578 total_max=lat["total_max"],
579 total_min=lat["total_min"],
580 last_max=lat["last_max"],
581 histogram=lat["histogram"],
582 dropped=lat_stats["err_cntrs"]["dropped"],
583 out_of_order=lat_stats["err_cntrs"]["out_of_order"],
584 duplicate=lat_stats["err_cntrs"]["dup"],
585 seq_too_high=lat_stats["err_cntrs"]["seq_too_high"],
586 seq_too_low=lat_stats["err_cntrs"]["seq_too_low"],
587 percentile_50=percentiles[0],
588 percentile_75=percentiles[1],
589 percentile_90=percentiles[2],
590 percentile_99=percentiles[3],
591 percentile_99_9=percentiles[4],
592 percentile_99_99=percentiles[5],
593 percentile_99_999=percentiles[6],
594 )
595 return ret
596
597 @staticmethod
598 def __get_readable_latency_stats(stats):
599 """
600 :param stats: LatencyStats
601 :return:
602 """
603 histogram = ""
604 # need to listify in order to be able to sort them.
605 l = list(stats.histogram.keys())
606 l.sort()
607 for sample in l:
608 range_start = sample
609 if range_start == 0:
610 range_end = 10
611 else:
612 range_end = range_start + pow(10, (len(str(range_start)) - 1))
613 val = stats.histogram[sample]
614 histogram = (
615 histogram
616 + "\n Packets with latency between {0:>5} us and {1:>5} us: {2:>10}".format(
617 range_start, range_end, val
618 )
619 )
620
621 return """
622 Latency info for pg_id {}
623 Dropped packets: {}
624 Out-of-order packets: {}
625 Sequence too high packets: {}
626 Sequence too low packets: {}
627 Maximum latency: {} us
628 Minimum latency: {} us
629 Maximum latency in last sampling period: {} us
630 Average latency: {} us
631 50th percentile latency: {} us
632 75th percentile latency: {} us
633 90th percentile latency: {} us
634 99th percentile latency: {} us
635 99.9th percentile latency: {} us
636 99.99th percentile latency: {} us
637 99.999th percentile latency: {} us
638 Jitter: {} us
639 Latency distribution histogram: {}
640 """.format(stats.pg_id, stats.dropped, stats.out_of_order,
641 stats.seq_too_high, stats.seq_too_low, stats.total_max,
642 stats.total_min, stats.last_max, stats.average,
643 stats.percentile_50, stats.percentile_75,
644 stats.percentile_90,
645 stats.percentile_99, stats.percentile_99_9,
646 stats.percentile_99_99,
647 stats.percentile_99_999, stats.jitter, histogram)
648
649 @staticmethod
650 def __get_flow_stats(pg_id, stats):
651 """
652 :param pg_id: int
653 :param stats:
654 :return:
655 """
656 FlowStats = collections.namedtuple(
657 "FlowStats",
658 ["pg_id", "tx_packets", "rx_packets", "tx_bytes", "rx_bytes", ],
659 )
660 flow_stats = stats["flow_stats"].get(pg_id)
661 ret = FlowStats(
662 pg_id=pg_id,
663 tx_packets=flow_stats["tx_pkts"]["total"],
664 rx_packets=flow_stats["rx_pkts"]["total"],
665 tx_bytes=flow_stats["tx_bytes"]["total"],
666 rx_bytes=flow_stats["rx_bytes"]["total"],
667 )
668 return ret
669
670 @staticmethod
671 def __get_readable_flow_stats(stats):
672 """
673 :param stats: FlowStats
674 :return:
675 """
676 return """Flow info for pg_id {}
677 TX packets: {}
678 RX packets: {}
679 TX bytes: {}
680 RX bytes: {}""".format(stats.pg_id, stats.tx_packets,
681 stats.rx_packets, stats.tx_bytes,
682 stats.rx_bytes)