T-Rex: 2.82, core pin, 8 workers
[csit.git] / GPL / tools / trex / trex_astf_profile.py
1 #!/usr/bin/python3
2
3 # Copyright (c) 2020 Cisco and/or its affiliates.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 """This module gets T-Rex advanced stateful (astf) traffic profile together
17 with other parameters, reads the profile and sends the traffic. At the end, it
18 measures the packet loss and latency.
19 """
20
21 import argparse
22 import json
23 import sys
24 import time
25
26 sys.path.insert(
27     0, u"/opt/trex-core-2.82/scripts/automation/trex_control_plane/interactive/"
28 )
29 from trex.astf.api import *
30
31
32 def fmt_latency(lat_min, lat_avg, lat_max, hdrh):
33     """Return formatted, rounded latency.
34
35     :param lat_min: Min latency
36     :param lat_avg: Average latency
37     :param lat_max: Max latency
38     :param hdrh: Base64 encoded compressed HDRHistogram object.
39     :type lat_min: str
40     :type lat_avg: str
41     :type lat_max: str
42     :type hdrh: str
43     :return: Formatted and rounded output (hdrh unchanged) "min/avg/max/hdrh".
44     :rtype: str
45     """
46     try:
47         t_min = int(round(float(lat_min)))
48     except ValueError:
49         t_min = int(-1)
50     try:
51         t_avg = int(round(float(lat_avg)))
52     except ValueError:
53         t_avg = int(-1)
54     try:
55         t_max = int(round(float(lat_max)))
56     except ValueError:
57         t_max = int(-1)
58
59     return u"/".join(str(tmp) for tmp in (t_min, t_avg, t_max, hdrh))
60
61
62 def simple_burst(
63         profile_file, duration, framesize, mult, warmup_time, port_0, port_1,
64         latency, async_start=False, traffic_directions=2):
65     """Send traffic and measure packet loss and latency.
66
67     Procedure:
68      - reads the given traffic profile with streams,
69      - connects to the T-rex astf client,
70      - resets the ports,
71      - removes all existing streams,
72      - adds streams from the traffic profile to the ports,
73      - if the warm-up time is more than 0, sends the warm-up traffic, reads the
74        statistics,
75      - clears the statistics from the client,
76      - starts the traffic,
77      - waits for the defined time (or runs forever if async mode is defined),
78      - stops the traffic,
79      - reads and displays the statistics and
80      - disconnects from the client.
81
82     :param profile_file: A python module with T-rex traffic profile.
83     :param duration: Duration of traffic run in seconds (-1=infinite).
84     :param framesize: Frame size.
85     :param mult: Multiplier of profile CPS.
86     :param warmup_time: Traffic warm-up time in seconds, 0 = disable.
87     :param port_0: Port 0 on the traffic generator.
88     :param port_1: Port 1 on the traffic generator.
89     :param latency: With latency stats.
90     :param async_start: Start the traffic and exit.
91     :param traffic_directions: Bidirectional (2) or unidirectional (1) traffic.
92     :type profile_file: str
93     :type duration: float
94     :type framesize: int or str
95     :type mult: int
96     :type warmup_time: float
97     :type port_0: int
98     :type port_1: int
99     :type latency: bool
100     :type async_start: bool
101     :type traffic_directions: int
102     """
103     client = None
104     total_rcvd = 0
105     total_sent = 0
106     lost_a = 0
107     lost_b = 0
108     lat_a = u"-1/-1/-1/"
109     lat_b = u"-1/-1/-1/"
110     lat_a_hist = u""
111     lat_b_hist = u""
112     l7_data = u""
113     stats = dict()
114     stats_sampling = 1.0
115     approximated_duration = 0
116
117     # Read the profile.
118     try:
119         # TODO: key-values pairs to the profile file
120         #  - ips ?
121         print(f"### Profile file:\n{profile_file}")
122         profile = ASTFProfile.load(profile_file, framesize=framesize)
123     except TRexError:
124         print(f"Error while loading profile '{profile_file}'!")
125         raise
126
127     try:
128         # Create the client.
129         client = ASTFClient()
130         # Connect to server
131         client.connect()
132         # Acquire ports, stop the traffic, remove loaded traffic and clear
133         # stats.
134         client.reset()
135         # Load the profile.
136         client.load_profile(profile)
137
138         ports = [port_0]
139         if traffic_directions > 1:
140             ports.append(port_1)
141
142         # Warm-up phase.
143         if warmup_time > 0:
144             # Clear the stats before injecting.
145             client.clear_stats()
146             # Choose CPS and start traffic.
147             client.start(mult=mult, duration=warmup_time)
148             time_start = time.monotonic()
149
150             # Read the stats after the warmup duration (no sampling needed).
151             time.sleep(warmup_time)
152             stats[time.monotonic()-time_start] = client.get_stats()
153
154             if client.get_warnings():
155                 for warning in client.get_warnings():
156                     print(warning)
157
158             client.reset()
159
160             print(u"##### Warmup Statistics #####")
161             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
162
163             # TODO: check stats format
164             stats = stats[sorted(stats.keys())[-1]]
165             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
166             if traffic_directions > 1:
167                 lost_b = stats[port_1][u"opackets"] - stats[port_0][u"ipackets"]
168
169             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
170             if traffic_directions > 1:
171                 print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
172
173         # Clear the stats before injecting.
174         lost_a = 0
175         lost_b = 0
176         stats = dict()
177
178         # Choose CPS and start traffic.
179         client.start(
180             mult=mult, duration=duration, nc=True,
181             latency_pps=mult if latency else 0, client_mask=2**len(ports)-1
182         )
183         time_start = time.monotonic()
184         # t-rex starts the packet flow with the delay
185         stats[time.monotonic()-time_start] = client.get_stats(ports=[port_0])
186         while stats[sorted(stats.keys())[-1]][port_0][u"opackets"] == 0:
187             stats.clear()
188             time.sleep(0.001)
189             stats[time.monotonic() - time_start] = \
190                 client.get_stats(ports=[port_0])
191         else:
192             trex_start_time = list(sorted(stats.keys()))[-1]
193             time_start += trex_start_time
194             stats.clear()
195
196         if async_start:
197             # For async stop, we need to export the current snapshot.
198             xsnap0 = client.ports[port_0].get_xstats().reference_stats
199             print(f"Xstats snapshot 0: {xsnap0!r}")
200             if traffic_directions > 1:
201                 xsnap1 = client.ports[port_1].get_xstats().reference_stats
202                 print(f"Xstats snapshot 1: {xsnap1!r}")
203         else:
204             time.sleep(
205                 stats_sampling if stats_sampling < duration else duration
206             )
207             # Do not block until done.
208             while client.is_traffic_active(ports=ports):
209                 # Sample the stats.
210                 stats[time.monotonic()-time_start] = \
211                     client.get_stats(ports=ports)
212                 time.sleep(
213                     stats_sampling if stats_sampling < duration else duration
214                 )
215             else:
216                 # Read the stats after the test
217                 stats[time.monotonic()-time_start] = \
218                     client.get_stats(ports=ports)
219
220             if client.get_warnings():
221                 for warning in client.get_warnings():
222                     print(warning)
223
224             client.reset()
225
226             print(u"##### Statistics #####")
227             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
228
229             approximated_duration = list(sorted(stats.keys()))[-1]
230             stats = stats[sorted(stats.keys())[-1]]
231             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
232             if traffic_directions > 1:
233                 lost_b = stats[port_1][u"opackets"] - stats[port_0][u"ipackets"]
234
235             # TODO: Latency measurement not used at this phase. This part will
236             #  be aligned in another commit.
237             # Stats index is not a port number, but "pgid".
238             if latency:
239                 lat_obj = stats[u"latency"][0][u"hist"]
240                 # TODO: Latency histogram is dictionary in astf mode,
241                 #  needs additional processing
242                 lat_a = fmt_latency(
243                     str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
244                     str(lat_obj[u"max_usec"]), u"-")
245                 lat_a_hist = str(lat_obj[u"histogram"])
246                 if traffic_directions > 1:
247                     lat_obj = stats[u"latency"][1][u"hist"]
248                     lat_b = fmt_latency(
249                         str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
250                         str(lat_obj[u"max_usec"]), u"-")
251                     lat_b_hist = str(lat_obj[u"histogram"])
252
253             if traffic_directions > 1:
254                 total_sent = \
255                     stats[port_0][u"opackets"] + stats[port_1][u"opackets"]
256                 total_rcvd = \
257                     stats[port_0][u"ipackets"] + stats[port_1][u"ipackets"]
258                 client_stats = stats[u"traffic"][u"client"]
259                 server_stats = stats[u"traffic"][u"server"]
260                 # Some zero counters are not sent
261                 # Active and established flows UDP/TCP
262                 # Client
263                 c_act_flows = client_stats[u"m_active_flows"]
264                 c_est_flows = client_stats[u"m_est_flows"]
265                 c_traffic_duration = client_stats.get(u"m_traffic_duration", 0)
266                 l7_data = f"client_active_flows={c_act_flows}, "
267                 l7_data += f"client_established_flows={c_est_flows}, "
268                 l7_data += f"client_traffic_duration={c_traffic_duration}, "
269                 # Possible errors
270                 # Too many packets in NIC rx queue
271                 c_err_rx_throttled = client_stats.get(u"err_rx_throttled", 0)
272                 l7_data += f"client_err_rx_throttled={c_err_rx_throttled}, "
273                 # Number of client side flows that were not opened
274                 # due to flow-table overflow
275                 c_err_nf_throttled = client_stats.get(u"err_c_nf_throttled", 0)
276                 l7_data += f"client_err_nf_throttled={c_err_nf_throttled}, "
277                 # Too many flows
278                 c_err_flow_overflow = client_stats.get(u"err_flow_overflow", 0)
279                 l7_data += f"client_err_flow_overflow={c_err_flow_overflow}, "
280                 # Server
281                 s_act_flows = server_stats[u"m_active_flows"]
282                 s_est_flows = server_stats[u"m_est_flows"]
283                 s_traffic_duration = server_stats.get(u"m_traffic_duration", 0)
284                 l7_data += f"server_active_flows={s_act_flows}, "
285                 l7_data += f"server_established_flows={s_est_flows}, "
286                 l7_data += f"server_traffic_duration={s_traffic_duration}, "
287                 # Possible errors
288                 # Too many packets in NIC rx queue
289                 s_err_rx_throttled = server_stats.get(u"err_rx_throttled", 0)
290                 l7_data += f"client_err_rx_throttled={s_err_rx_throttled}, "
291                 if u"udp" in profile_file:
292                     # Client
293                     # Established connections
294                     c_udp_connects = client_stats.get(u"udps_connects", 0)
295                     l7_data += f"client_udp_connects={c_udp_connects}, "
296                     # Closed connections
297                     c_udp_closed = client_stats.get(u"udps_closed", 0)
298                     l7_data += f"client_udp_closed={c_udp_closed}, "
299                     # Sent bytes
300                     c_udp_sndbyte = client_stats.get(u"udps_sndbyte", 0)
301                     l7_data += f"client_udp_tx_bytes={c_udp_sndbyte}, "
302                     # Sent packets
303                     c_udp_sndpkt = client_stats.get(u"udps_sndpkt", 0)
304                     l7_data += f"client_udp_tx_packets={c_udp_sndpkt}, "
305                     # Received bytes
306                     c_udp_rcvbyte = client_stats.get(u"udps_rcvbyte", 0)
307                     l7_data += f"client_udp_rx_bytes={c_udp_rcvbyte}, "
308                     # Received packets
309                     c_udp_rcvpkt = client_stats.get(u"udps_rcvpkt", 0)
310                     l7_data += f"client_udp_rx_packets={c_udp_rcvpkt}, "
311                     # Keep alive drops
312                     c_udp_keepdrops = client_stats.get(u"udps_keepdrops", 0)
313                     l7_data += f"client_udp_keep_drops={c_udp_keepdrops}, "
314                     # Server
315                     # Accepted connections
316                     s_udp_accepts = server_stats.get(u"udps_accepts", 0)
317                     l7_data += f"server_udp_accepts={s_udp_accepts}, "
318                     # Closed connections
319                     s_udp_closed = server_stats.get(u"udps_closed", 0)
320                     l7_data += f"server_udp_closed={s_udp_closed}, "
321                     # Sent bytes
322                     s_udp_sndbyte = server_stats.get(u"udps_sndbyte", 0)
323                     l7_data += f"server_udp_tx_bytes={s_udp_sndbyte}, "
324                     # Sent packets
325                     s_udp_sndpkt = server_stats.get(u"udps_sndpkt", 0)
326                     l7_data += f"server_udp_tx_packets={s_udp_sndpkt}, "
327                     # Received bytes
328                     s_udp_rcvbyte = server_stats.get(u"udps_rcvbyte", 0)
329                     l7_data += f"server_udp_rx_bytes={s_udp_rcvbyte}, "
330                     # Received packets
331                     s_udp_rcvpkt = server_stats.get(u"udps_rcvpkt", 0)
332                     l7_data += f"server_udp_rx_packets={s_udp_rcvpkt}, "
333                 elif u"tcp" in profile_file:
334                     # Client
335                     # Initiated connections
336                     c_tcp_connatt = client_stats.get(u"tcps_connattempt", 0)
337                     l7_data += f"client_tcp_connect_inits={c_tcp_connatt}, "
338                     # Established connections
339                     c_tcp_connects = client_stats.get(u"tcps_connects", 0)
340                     l7_data += f"client_tcp_connects={c_tcp_connects}, "
341                     # Closed connections
342                     c_tcp_closed = client_stats.get(u"tcps_closed", 0)
343                     l7_data += f"client_tcp_closed={c_tcp_closed}, "
344                     # Send bytes
345                     c_tcp_sndbyte = client_stats.get(u"tcps_sndbyte", 0)
346                     l7_data += f"client_tcp_tx_bytes={c_tcp_sndbyte}, "
347                     # Received bytes
348                     c_tcp_rcvbyte = client_stats.get(u"tcps_rcvbyte", 0)
349                     l7_data += f"client_tcp_rx_bytes={c_tcp_rcvbyte}, "
350                     # Server
351                     # Accepted connections
352                     s_tcp_accepts = server_stats.get(u"tcps_accepts", 0)
353                     l7_data += f"server_tcp_accepts={s_tcp_accepts}, "
354                     # Established connections
355                     s_tcp_connects = server_stats.get(u"tcps_connects", 0)
356                     l7_data += f"server_tcp_connects={s_tcp_connects}, "
357                     # Closed connections
358                     s_tcp_closed = server_stats.get(u"tcps_closed", 0)
359                     l7_data += f"server_tcp_closed={s_tcp_closed}, "
360                     # Sent bytes
361                     s_tcp_sndbyte = server_stats.get(u"tcps_sndbyte", 0)
362                     l7_data += f"server_tcp_tx_bytes={s_tcp_sndbyte}, "
363                     # Received bytes
364                     s_tcp_rcvbyte = server_stats.get(u"tcps_rcvbyte", 0)
365                     l7_data += f"server_tcp_rx_bytes={s_tcp_rcvbyte}, "
366             else:
367                 total_sent = stats[port_0][u"opackets"]
368                 total_rcvd = stats[port_1][u"ipackets"]
369
370             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
371             if traffic_directions > 1:
372                 print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
373
374     except TRexError:
375         print(u"T-Rex ASTF runtime error!", file=sys.stderr)
376         raise
377
378     finally:
379         if client:
380             if async_start:
381                 client.disconnect(stop_traffic=False, release_ports=True)
382             else:
383                 client.clear_profile()
384                 client.disconnect()
385                 print(
386                     f"trex_start_time={trex_start_time}, "
387                     f"cps={mult!r}, total_received={total_rcvd}, "
388                     f"total_sent={total_sent}, frame_loss={lost_a + lost_b}, "
389                     f"approximated_duration={approximated_duration}, "
390                     f"latency_stream_0(usec)={lat_a}, "
391                     f"latency_stream_1(usec)={lat_b}, "
392                     f"latency_hist_stream_0={lat_a_hist}, "
393                     f"latency_hist_stream_1={lat_b_hist}, "
394                     f"{l7_data}"
395                 )
396
397
398 def main():
399     """Main function for the traffic generator using T-rex.
400
401     It verifies the given command line arguments and runs "simple_burst"
402     function.
403     """
404     parser = argparse.ArgumentParser()
405     parser.add_argument(
406         u"-p", u"--profile", required=True, type=str,
407         help=u"Python traffic profile."
408     )
409     parser.add_argument(
410         u"-d", u"--duration", required=True, type=float,
411         help=u"Duration of traffic run."
412     )
413     parser.add_argument(
414         u"-s", u"--frame_size", required=True,
415         help=u"Size of a Frame without padding and IPG."
416     )
417     parser.add_argument(
418         u"-m", u"--mult", required=True, type=int,
419         help=u"Multiplier of profile CPS."
420     )
421     parser.add_argument(
422         u"-w", u"--warmup_time", type=float, default=5.0,
423         help=u"Traffic warm-up time in seconds, 0 = disable."
424     )
425     parser.add_argument(
426         u"--port_0", required=True, type=int,
427         help=u"Port 0 on the traffic generator."
428     )
429     parser.add_argument(
430         u"--port_1", required=True, type=int,
431         help=u"Port 1 on the traffic generator."
432     )
433     parser.add_argument(
434         u"--async_start", action=u"store_true", default=False,
435         help=u"Non-blocking call of the script."
436     )
437     parser.add_argument(
438         u"--latency", action=u"store_true", default=False,
439         help=u"Add latency stream."
440     )
441     parser.add_argument(
442         u"--traffic_directions", type=int, default=2,
443         help=u"Send bi- (2) or uni- (1) directional traffic."
444     )
445
446     args = parser.parse_args()
447
448     try:
449         framesize = int(args.frame_size)
450     except ValueError:
451         framesize = args.frame_size
452
453     simple_burst(
454         profile_file=args.profile, duration=args.duration, framesize=framesize,
455         mult=args.mult, warmup_time=args.warmup_time, port_0=args.port_0,
456         port_1=args.port_1, latency=args.latency, async_start=args.async_start,
457         traffic_directions=args.traffic_directions
458     )
459
460
461 if __name__ == u"__main__":
462     main()