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