63831e4bf4f7a82c882f3ce8a2c1df78f12372d3
[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 parses for various counters.
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,
64         duration,
65         framesize,
66         multiplier,
67         port_0,
68         port_1,
69         latency,
70         async_start=False,
71         traffic_directions=2,
72     ):
73     """Send traffic and measure packet loss and latency.
74
75     Procedure:
76      - reads the given traffic profile with streams,
77      - connects to the T-rex astf client,
78      - resets the ports,
79      - removes all existing streams,
80      - adds streams from the traffic profile to the ports,
81      - clears the statistics from the client,
82      - starts the traffic,
83      - waits for the defined time (or runs forever if async mode is defined),
84      - explicitly stops the traffic,
85      - reads and displays the statistics and
86      - disconnects from the client.
87
88     Duration details:
89     Contrary to stateless mode, ASTF profiles typically limit the number
90     of flows/transactions that can happen.
91     The caller is expected to set the duration parameter accordingly to
92     this limit and multiplier, including any overheads.
93     See *_traffic_duration output fields for TRex's measurement
94     of the real traffic duration (should be without any inactivity overheads).
95     If traffic has not ended by the final time, the traffic
96     is stopped explicitly, counters reflect the state just after the stop.
97
98     TODO: Support tests which focus only on some transaction phases,
99     e.g. TCP tests ignoring init and teardown separated by delays.
100     Currently, approximated time measures the whole traffic duration.
101
102     :param profile_file: A python module with T-rex traffic profile.
103     :param duration: Expected duration for all transactions to finish,
104         assuming only tolerable duration stretching happens.
105         This includes later start of later transactions
106         (according to TPS multiplier) and expected duration of each transaction.
107         Critically, this also includes any delay TRex shows when starting
108         traffic (but not the similar delay during stopping).
109     :param framesize: Frame size.
110     :param multiplier: Multiplier of profile CPS.
111     :param port_0: Port 0 on the traffic generator.
112     :param port_1: Port 1 on the traffic generator.
113     :param latency: With latency stats.
114     :param async_start: Start the traffic and exit.
115     :param traffic_directions: Bidirectional (2) or unidirectional (1) traffic.
116     :type profile_file: str
117     :type duration: float
118     :type framesize: int or str
119     :type multiplier: int
120     :type port_0: int
121     :type port_1: int
122     :type latency: bool
123     :type async_start: bool
124     :type traffic_directions: int
125     """
126     client = None
127     total_received = 0
128     total_sent = 0
129     lost_a = 0
130     lost_b = 0
131     lat_a = u"-1/-1/-1/"
132     lat_b = u"-1/-1/-1/"
133     lat_a_hist = u""
134     lat_b_hist = u""
135     l7_data = u""
136     stats = dict()
137     approximated_duration = 0
138
139     # Read the profile.
140     try:
141         # TODO: key-values pairs to the profile file
142         #  - ips ?
143         print(f"### Profile file:\n{profile_file}")
144         profile = ASTFProfile.load(profile_file, framesize=framesize)
145     except TRexError:
146         print(f"Error while loading profile '{profile_file}'!")
147         raise
148
149     try:
150         # Create the client.
151         client = ASTFClient()
152         # Connect to server
153         client.connect()
154         # Acquire ports, stop the traffic, remove loaded traffic and clear
155         # stats.
156         client.reset()
157         # Load the profile.
158         client.load_profile(profile)
159
160         ports = [port_0]
161         if traffic_directions > 1:
162             ports.append(port_1)
163
164         # Clear the stats before injecting.
165         lost_a = 0
166         lost_b = 0
167         stats = dict()
168
169         # Choose CPS and start traffic.
170         client.start(
171             mult=multiplier,
172             # Increase the input duration slightly,
173             # to ensure it does not end before sleep&stop below happens.
174             duration=duration + 0.1 if duration > 0 else duration,
175             nc=True,
176             latency_pps=int(multiplier) if latency else 0,
177             client_mask=2**len(ports)-1,
178         )
179         time_start = time.monotonic()
180
181         if async_start:
182             # For async stop, we need to export the current snapshot.
183             xsnap0 = client.ports[port_0].get_xstats().reference_stats
184             print(f"Xstats snapshot 0: {xsnap0!r}")
185             if traffic_directions > 1:
186                 xsnap1 = client.ports[port_1].get_xstats().reference_stats
187                 print(f"Xstats snapshot 1: {xsnap1!r}")
188         else:
189             time.sleep(duration)
190
191             # Do not block yet, the existing transactions may take long time
192             # to finish. We need an action that is almost reset(),
193             # but without clearing stats.
194             client.stop(block=False)
195             client.stop_latency()
196             client.remove_rx_queue(client.get_all_ports())
197             # Now we can wait for the real traffic stop.
198             client.stop(block=True)
199
200             # Read the stats after the traffic stopped (or time up).
201             stats[time.monotonic() - time_start] = client.get_stats(
202                 ports=ports
203             )
204
205             if client.get_warnings():
206                 for warning in client.get_warnings():
207                     print(warning)
208
209             # Now finish the complete reset.
210             client.reset()
211
212             print(u"##### Statistics #####")
213             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
214
215             approximated_duration = list(sorted(stats.keys()))[-1]
216             stats = stats[sorted(stats.keys())[-1]]
217             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
218             if traffic_directions > 1:
219                 lost_b = stats[port_1][u"opackets"] - stats[port_0][u"ipackets"]
220
221             # TODO: Latency measurement not used at this phase. This part will
222             #  be aligned in another commit.
223             # Stats index is not a port number, but "pgid".
224             if latency:
225                 lat_obj = stats[u"latency"][0][u"hist"]
226                 # TODO: Latency histogram is dictionary in astf mode,
227                 #  needs additional processing
228                 lat_a = fmt_latency(
229                     str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
230                     str(lat_obj[u"max_usec"]), u"-")
231                 lat_a_hist = str(lat_obj[u"histogram"])
232                 if traffic_directions > 1:
233                     lat_obj = stats[u"latency"][1][u"hist"]
234                     lat_b = fmt_latency(
235                         str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
236                         str(lat_obj[u"max_usec"]), u"-")
237                     lat_b_hist = str(lat_obj[u"histogram"])
238
239             if traffic_directions > 1:
240                 total_sent = \
241                     stats[port_0][u"opackets"] + stats[port_1][u"opackets"]
242                 total_received = \
243                     stats[port_0][u"ipackets"] + stats[port_1][u"ipackets"]
244                 client_sent = stats[port_0][u"opackets"]
245                 client_received = stats[port_0][u"ipackets"]
246                 client_stats = stats[u"traffic"][u"client"]
247                 server_stats = stats[u"traffic"][u"server"]
248                 # Some zero counters are not sent
249                 # Active and established flows UDP/TCP
250                 # Client
251                 c_act_flows = client_stats[u"m_active_flows"]
252                 c_est_flows = client_stats[u"m_est_flows"]
253                 c_traffic_duration = client_stats.get(u"m_traffic_duration", 0)
254                 l7_data = f"client_active_flows={c_act_flows}; "
255                 l7_data += f"client_established_flows={c_est_flows}; "
256                 l7_data += f"client_traffic_duration={c_traffic_duration}; "
257                 # Possible errors
258                 # Too many packets in NIC rx queue
259                 c_err_rx_throttled = client_stats.get(u"err_rx_throttled", 0)
260                 l7_data += f"client_err_rx_throttled={c_err_rx_throttled}; "
261                 # Number of client side flows that were not opened
262                 # due to flow-table overflow
263                 c_err_nf_throttled = client_stats.get(u"err_c_nf_throttled", 0)
264                 l7_data += f"client_err_nf_throttled={c_err_nf_throttled}; "
265                 # Too many flows
266                 c_err_flow_overflow = client_stats.get(u"err_flow_overflow", 0)
267                 l7_data += f"client_err_flow_overflow={c_err_flow_overflow}; "
268                 # Server
269                 s_act_flows = server_stats[u"m_active_flows"]
270                 s_est_flows = server_stats[u"m_est_flows"]
271                 s_traffic_duration = server_stats.get(u"m_traffic_duration", 0)
272                 l7_data += f"server_active_flows={s_act_flows}; "
273                 l7_data += f"server_established_flows={s_est_flows}; "
274                 l7_data += f"server_traffic_duration={s_traffic_duration}; "
275                 # Possible errors
276                 # Too many packets in NIC rx queue
277                 s_err_rx_throttled = server_stats.get(u"err_rx_throttled", 0)
278                 l7_data += f"client_err_rx_throttled={s_err_rx_throttled}; "
279                 if u"udp" in profile_file:
280                     # Client
281                     # Established connections
282                     c_udp_connects = client_stats.get(u"udps_connects", 0)
283                     l7_data += f"client_udp_connects={c_udp_connects}; "
284                     # Closed connections
285                     c_udp_closed = client_stats.get(u"udps_closed", 0)
286                     l7_data += f"client_udp_closed={c_udp_closed}; "
287                     # Sent bytes
288                     c_udp_sndbyte = client_stats.get(u"udps_sndbyte", 0)
289                     l7_data += f"client_udp_tx_bytes={c_udp_sndbyte}; "
290                     # Sent packets
291                     c_udp_sndpkt = client_stats.get(u"udps_sndpkt", 0)
292                     l7_data += f"client_udp_tx_packets={c_udp_sndpkt}; "
293                     # Received bytes
294                     c_udp_rcvbyte = client_stats.get(u"udps_rcvbyte", 0)
295                     l7_data += f"client_udp_rx_bytes={c_udp_rcvbyte}; "
296                     # Received packets
297                     c_udp_rcvpkt = client_stats.get(u"udps_rcvpkt", 0)
298                     l7_data += f"client_udp_rx_packets={c_udp_rcvpkt}; "
299                     # Keep alive drops
300                     c_udp_keepdrops = client_stats.get(u"udps_keepdrops", 0)
301                     l7_data += f"client_udp_keep_drops={c_udp_keepdrops}; "
302                     # Client without flow
303                     c_err_cwf = client_stats.get(u"err_cwf", 0)
304                     l7_data += f"client_err_cwf={c_err_cwf}; "
305                     # Server
306                     # Accepted connections
307                     s_udp_accepts = server_stats.get(u"udps_accepts", 0)
308                     l7_data += f"server_udp_accepts={s_udp_accepts}; "
309                     # Closed connections
310                     s_udp_closed = server_stats.get(u"udps_closed", 0)
311                     l7_data += f"server_udp_closed={s_udp_closed}; "
312                     # Sent bytes
313                     s_udp_sndbyte = server_stats.get(u"udps_sndbyte", 0)
314                     l7_data += f"server_udp_tx_bytes={s_udp_sndbyte}; "
315                     # Sent packets
316                     s_udp_sndpkt = server_stats.get(u"udps_sndpkt", 0)
317                     l7_data += f"server_udp_tx_packets={s_udp_sndpkt}; "
318                     # Received bytes
319                     s_udp_rcvbyte = server_stats.get(u"udps_rcvbyte", 0)
320                     l7_data += f"server_udp_rx_bytes={s_udp_rcvbyte}; "
321                     # Received packets
322                     s_udp_rcvpkt = server_stats.get(u"udps_rcvpkt", 0)
323                     l7_data += f"server_udp_rx_packets={s_udp_rcvpkt}; "
324                 elif u"tcp" in profile_file:
325                     # Client
326                     # Connection attempts
327                     c_tcp_connattempt = client_stats.get(u"tcps_connattempt", 0)
328                     l7_data += f"client_tcp_connattempt={c_tcp_connattempt}; "
329                     # Established connections
330                     c_tcp_connects = client_stats.get(u"tcps_connects", 0)
331                     l7_data += f"client_tcp_connects={c_tcp_connects}; "
332                     # Closed connections
333                     c_tcp_closed = client_stats.get(u"tcps_closed", 0)
334                     l7_data += f"client_tcp_closed={c_tcp_closed}; "
335                     # Send bytes
336                     c_tcp_sndbyte = client_stats.get(u"tcps_sndbyte", 0)
337                     l7_data += f"client_tcp_tx_bytes={c_tcp_sndbyte}; "
338                     # Received bytes
339                     c_tcp_rcvbyte = client_stats.get(u"tcps_rcvbyte", 0)
340                     l7_data += f"client_tcp_rx_bytes={c_tcp_rcvbyte}; "
341                     # Server
342                     # Accepted connections
343                     s_tcp_accepts = server_stats.get(u"tcps_accepts", 0)
344                     l7_data += f"server_tcp_accepts={s_tcp_accepts}; "
345                     # Established connections
346                     s_tcp_connects = server_stats.get(u"tcps_connects", 0)
347                     l7_data += f"server_tcp_connects={s_tcp_connects}; "
348                     # Closed connections
349                     s_tcp_closed = server_stats.get(u"tcps_closed", 0)
350                     l7_data += f"server_tcp_closed={s_tcp_closed}; "
351                     # Sent bytes
352                     s_tcp_sndbyte = server_stats.get(u"tcps_sndbyte", 0)
353                     l7_data += f"server_tcp_tx_bytes={s_tcp_sndbyte}; "
354                     # Received bytes
355                     s_tcp_rcvbyte = server_stats.get(u"tcps_rcvbyte", 0)
356                     l7_data += f"server_tcp_rx_bytes={s_tcp_rcvbyte}; "
357             else:
358                 total_sent = stats[port_0][u"opackets"]
359                 total_received = stats[port_1][u"ipackets"]
360
361             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
362             if traffic_directions > 1:
363                 print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
364
365     except TRexError:
366         print(u"T-Rex ASTF runtime error!", file=sys.stderr)
367         raise
368
369     finally:
370         if client:
371             if async_start:
372                 client.disconnect(stop_traffic=False, release_ports=True)
373             else:
374                 client.clear_profile()
375                 client.disconnect()
376                 print(
377                     f"multiplier={multiplier!r}; "
378                     f"total_received={total_received}; "
379                     f"total_sent={total_sent}; "
380                     f"frame_loss={lost_a + lost_b}; "
381                     f"approximated_duration={approximated_duration}; "
382                     f"latency_stream_0(usec)={lat_a}; "
383                     f"latency_stream_1(usec)={lat_b}; "
384                     f"latency_hist_stream_0={lat_a_hist}; "
385                     f"latency_hist_stream_1={lat_b_hist}; "
386                     f"client_sent={client_sent}; "
387                     f"client_received={client_received}; "
388                     f"{l7_data}"
389                 )
390
391
392 def main():
393     """Main function for the traffic generator using T-rex.
394
395     It verifies the given command line arguments and runs "simple_burst"
396     function.
397     """
398     parser = argparse.ArgumentParser()
399     parser.add_argument(
400         u"-p", u"--profile", required=True, type=str,
401         help=u"Python traffic profile."
402     )
403     parser.add_argument(
404         u"-d", u"--duration", required=True, type=float,
405         help=u"Duration of the whole traffic run, including overheads."
406     )
407     parser.add_argument(
408         u"-s", u"--frame_size", required=True,
409         help=u"Size of a Frame without padding and IPG."
410     )
411     parser.add_argument(
412         u"-m", u"--multiplier", required=True, type=float,
413         help=u"Multiplier of profile CPS."
414     )
415     parser.add_argument(
416         u"--port_0", required=True, type=int,
417         help=u"Port 0 on the traffic generator."
418     )
419     parser.add_argument(
420         u"--port_1", required=True, type=int,
421         help=u"Port 1 on the traffic generator."
422     )
423     parser.add_argument(
424         u"--async_start", action=u"store_true", default=False,
425         help=u"Non-blocking call of the script."
426     )
427     parser.add_argument(
428         u"--latency", action=u"store_true", default=False,
429         help=u"Add latency stream."
430     )
431     parser.add_argument(
432         u"--traffic_directions", type=int, default=2,
433         help=u"Send bi- (2) or uni- (1) directional traffic."
434     )
435
436     args = parser.parse_args()
437
438     try:
439         framesize = int(args.frame_size)
440     except ValueError:
441         framesize = args.frame_size
442
443     simple_burst(
444         profile_file=args.profile,
445         duration=args.duration,
446         framesize=framesize,
447         multiplier=args.multiplier,
448         port_0=args.port_0,
449         port_1=args.port_1,
450         latency=args.latency,
451         async_start=args.async_start,
452         traffic_directions=args.traffic_directions,
453     )
454
455
456 if __name__ == u"__main__":
457     main()