feat(astf): Support framesizes for ASTF
[csit.git] / GPL / tools / trex / trex_astf_profile.py
1 #!/usr/bin/python3
2
3 # Copyright (c) 2022 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 ASTFClient, ASTFProfile, TRexError
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         n_data_frames,
77         multiplier,
78         port_0,
79         port_1,
80         latency,
81         async_start=False,
82         traffic_directions=2,
83         delay=0.0,
84     ):
85     """Send traffic and measure packet loss and latency.
86
87     Procedure:
88      - reads the given traffic profile with streams,
89      - connects to the T-rex astf client,
90      - resets the ports,
91      - removes all existing streams,
92      - adds streams from the traffic profile to the ports,
93      - clears the statistics from the client,
94      - starts the traffic,
95      - waits for the defined time (or runs forever if async mode is defined),
96      - explicitly stops the traffic,
97      - reads and displays the statistics and
98      - disconnects from the client.
99
100     Duration details:
101     Contrary to stateless mode, ASTF profiles typically limit the number
102     of flows/transactions that can happen.
103     The caller is expected to set the duration parameter to idealized value,
104     but set the delay arguments when TRex is expected
105     to finish processing replies later (including a window for latency).
106     See *_traffic_duration output fields for TRex's measurement
107     of the real traffic duration (should be without any inactivity overheads).
108     If traffic has not ended by the final time, the traffic
109     is stopped explicitly, counters reflect the state just after the stop.
110
111     TODO: Support tests which focus only on some transaction phases,
112     e.g. TCP tests ignoring init and teardown separated by delays.
113     Currently, approximated time measures the whole traffic duration.
114
115     :param profile_file: A python module with T-rex traffic profile.
116     :param duration: Expected duration for all transactions to finish,
117         without any TRex related delays, without even latency.
118     :param framesize: Frame size.
119     :param n_data_frames: Controls "size" of transaction for TPUT tests.
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     :param delay: Time increase [s] for sleep duration.
127     :type profile_file: str
128     :type duration: float
129     :type framesize: int or str
130     :type n_data_frames: int
131     :type multiplier: int
132     :type port_0: int
133     :type port_1: int
134     :type latency: bool
135     :type async_start: bool
136     :type traffic_directions: int
137     :type delay: float
138     """
139     client = None
140     total_received = 0
141     total_sent = 0
142     lost_a = 0
143     lost_b = 0
144     lat_a = u"-1/-1/-1/"
145     lat_b = u"-1/-1/-1/"
146     lat_a_hist = u""
147     lat_b_hist = u""
148     l7_data = u""
149     stats = dict()
150     approximated_duration = 0
151
152     # Read the profile.
153     try:
154         # TODO: key-values pairs to the profile file
155         #  - ips ?
156         print(f"### Profile file:\n{profile_file}")
157         profile = ASTFProfile.load(
158             profile_file,
159             framesize=framesize,
160             n_data_frames=n_data_frames,
161         )
162     except TRexError:
163         print(f"Error while loading profile '{profile_file}'!")
164         raise
165
166     try:
167         # Create the client.
168         client = ASTFClient()
169         # Connect to server
170         client.connect()
171         # Acquire ports, stop the traffic, remove loaded traffic and clear
172         # stats.
173         client.reset()
174         # Load the profile.
175         client.load_profile(profile)
176
177         ports = [port_0]
178         if traffic_directions > 1:
179             ports.append(port_1)
180
181         # Clear the stats before injecting.
182         lost_a = 0
183         lost_b = 0
184         stats = dict()
185
186         # Choose CPS and start traffic.
187         client.start(
188             mult=multiplier,
189             duration=duration,
190             nc=True,
191             latency_pps=int(multiplier) if latency else 0,
192             client_mask=2**len(ports)-1,
193         )
194         time_stop = time.monotonic() + duration + delay
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(duration + delay)
205             # Do not block yet, the existing transactions may take long time
206             # to finish. We need an action that is almost reset(),
207             # but without clearing stats.
208             client.stop(block=False)
209             client.stop_latency()
210             client.remove_rx_queue(client.get_all_ports())
211             # Now we can wait for the real traffic stop.
212             client.stop(block=True)
213
214             # Read the stats after the traffic stopped (or time up).
215             stats[time.monotonic() - time_stop] = client.get_stats(
216                 ports=ports
217             )
218
219             if client.get_warnings():
220                 for warning in client.get_warnings():
221                     print(warning)
222
223             # Now finish the complete reset.
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_received = \
257                     stats[port_0][u"ipackets"] + stats[port_1][u"ipackets"]
258                 client_sent = stats[port_0][u"opackets"]
259                 client_received = stats[port_0][u"ipackets"]
260                 client_stats = stats[u"traffic"][u"client"]
261                 server_stats = stats[u"traffic"][u"server"]
262                 # Some zero counters are not sent
263                 # Active and established flows UDP/TCP
264                 # Client
265                 c_act_flows = client_stats[u"m_active_flows"]
266                 c_est_flows = client_stats[u"m_est_flows"]
267                 c_traffic_duration = client_stats.get(u"m_traffic_duration", 0)
268                 l7_data = f"client_active_flows={c_act_flows}; "
269                 l7_data += f"client_established_flows={c_est_flows}; "
270                 l7_data += f"client_traffic_duration={c_traffic_duration}; "
271                 # Possible errors
272                 # Too many packets in NIC rx queue
273                 c_err_rx_throttled = client_stats.get(u"err_rx_throttled", 0)
274                 l7_data += f"client_err_rx_throttled={c_err_rx_throttled}; "
275                 # Number of client side flows that were not opened
276                 # due to flow-table overflow
277                 c_err_nf_throttled = client_stats.get(u"err_c_nf_throttled", 0)
278                 l7_data += f"client_err_nf_throttled={c_err_nf_throttled}; "
279                 # Too many flows
280                 c_err_flow_overflow = client_stats.get(u"err_flow_overflow", 0)
281                 l7_data += f"client_err_flow_overflow={c_err_flow_overflow}; "
282                 # Server
283                 s_act_flows = server_stats[u"m_active_flows"]
284                 s_est_flows = server_stats[u"m_est_flows"]
285                 s_traffic_duration = server_stats.get(u"m_traffic_duration", 0)
286                 l7_data += f"server_active_flows={s_act_flows}; "
287                 l7_data += f"server_established_flows={s_est_flows}; "
288                 l7_data += f"server_traffic_duration={s_traffic_duration}; "
289                 # Possible errors
290                 # Too many packets in NIC rx queue
291                 s_err_rx_throttled = server_stats.get(u"err_rx_throttled", 0)
292                 l7_data += f"client_err_rx_throttled={s_err_rx_throttled}; "
293                 if u"udp" in profile_file:
294                     # Client
295                     # Established connections
296                     c_udp_connects = client_stats.get(u"udps_connects", 0)
297                     l7_data += f"client_udp_connects={c_udp_connects}; "
298                     # Closed connections
299                     c_udp_closed = client_stats.get(u"udps_closed", 0)
300                     l7_data += f"client_udp_closed={c_udp_closed}; "
301                     # Sent bytes
302                     c_udp_sndbyte = client_stats.get(u"udps_sndbyte", 0)
303                     l7_data += f"client_udp_tx_bytes={c_udp_sndbyte}; "
304                     # Sent packets
305                     c_udp_sndpkt = client_stats.get(u"udps_sndpkt", 0)
306                     l7_data += f"client_udp_tx_packets={c_udp_sndpkt}; "
307                     # Received bytes
308                     c_udp_rcvbyte = client_stats.get(u"udps_rcvbyte", 0)
309                     l7_data += f"client_udp_rx_bytes={c_udp_rcvbyte}; "
310                     # Received packets
311                     c_udp_rcvpkt = client_stats.get(u"udps_rcvpkt", 0)
312                     l7_data += f"client_udp_rx_packets={c_udp_rcvpkt}; "
313                     # Keep alive drops
314                     c_udp_keepdrops = client_stats.get(u"udps_keepdrops", 0)
315                     l7_data += f"client_udp_keep_drops={c_udp_keepdrops}; "
316                     # Client without flow
317                     c_err_cwf = client_stats.get(u"err_cwf", 0)
318                     l7_data += f"client_err_cwf={c_err_cwf}; "
319                     # Server
320                     # Accepted connections
321                     s_udp_accepts = server_stats.get(u"udps_accepts", 0)
322                     l7_data += f"server_udp_accepts={s_udp_accepts}; "
323                     # Closed connections
324                     s_udp_closed = server_stats.get(u"udps_closed", 0)
325                     l7_data += f"server_udp_closed={s_udp_closed}; "
326                     # Sent bytes
327                     s_udp_sndbyte = server_stats.get(u"udps_sndbyte", 0)
328                     l7_data += f"server_udp_tx_bytes={s_udp_sndbyte}; "
329                     # Sent packets
330                     s_udp_sndpkt = server_stats.get(u"udps_sndpkt", 0)
331                     l7_data += f"server_udp_tx_packets={s_udp_sndpkt}; "
332                     # Received bytes
333                     s_udp_rcvbyte = server_stats.get(u"udps_rcvbyte", 0)
334                     l7_data += f"server_udp_rx_bytes={s_udp_rcvbyte}; "
335                     # Received packets
336                     s_udp_rcvpkt = server_stats.get(u"udps_rcvpkt", 0)
337                     l7_data += f"server_udp_rx_packets={s_udp_rcvpkt}; "
338                 elif u"tcp" in profile_file:
339                     # Client
340                     # Connection attempts
341                     c_tcp_connattempt = client_stats.get(u"tcps_connattempt", 0)
342                     l7_data += f"client_tcp_connattempt={c_tcp_connattempt}; "
343                     # Established connections
344                     c_tcp_connects = client_stats.get(u"tcps_connects", 0)
345                     l7_data += f"client_tcp_connects={c_tcp_connects}; "
346                     # Closed connections
347                     c_tcp_closed = client_stats.get(u"tcps_closed", 0)
348                     l7_data += f"client_tcp_closed={c_tcp_closed}; "
349                     # Send bytes
350                     c_tcp_sndbyte = client_stats.get(u"tcps_sndbyte", 0)
351                     l7_data += f"client_tcp_tx_bytes={c_tcp_sndbyte}; "
352                     # Received bytes
353                     c_tcp_rcvbyte = client_stats.get(u"tcps_rcvbyte", 0)
354                     l7_data += f"client_tcp_rx_bytes={c_tcp_rcvbyte}; "
355                     # Server
356                     # Accepted connections
357                     s_tcp_accepts = server_stats.get(u"tcps_accepts", 0)
358                     l7_data += f"server_tcp_accepts={s_tcp_accepts}; "
359                     # Established connections
360                     s_tcp_connects = server_stats.get(u"tcps_connects", 0)
361                     l7_data += f"server_tcp_connects={s_tcp_connects}; "
362                     # Closed connections
363                     s_tcp_closed = server_stats.get(u"tcps_closed", 0)
364                     l7_data += f"server_tcp_closed={s_tcp_closed}; "
365                     # Sent bytes
366                     s_tcp_sndbyte = server_stats.get(u"tcps_sndbyte", 0)
367                     l7_data += f"server_tcp_tx_bytes={s_tcp_sndbyte}; "
368                     # Received bytes
369                     s_tcp_rcvbyte = server_stats.get(u"tcps_rcvbyte", 0)
370                     l7_data += f"server_tcp_rx_bytes={s_tcp_rcvbyte}; "
371             else:
372                 total_sent = stats[port_0][u"opackets"]
373                 total_received = stats[port_1][u"ipackets"]
374
375             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
376             if traffic_directions > 1:
377                 print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
378
379     except TRexError:
380         print(u"T-Rex ASTF runtime error!", file=sys.stderr)
381         raise
382
383     finally:
384         if client:
385             if async_start:
386                 client.disconnect(stop_traffic=False, release_ports=True)
387             else:
388                 client.clear_profile()
389                 client.disconnect()
390                 print(
391                     f"multiplier={multiplier!r}; "
392                     f"total_received={total_received}; "
393                     f"total_sent={total_sent}; "
394                     f"frame_loss={lost_a + lost_b}; "
395                     f"approximated_duration={approximated_duration}; "
396                     f"latency_stream_0(usec)={lat_a}; "
397                     f"latency_stream_1(usec)={lat_b}; "
398                     f"latency_hist_stream_0={lat_a_hist}; "
399                     f"latency_hist_stream_1={lat_b_hist}; "
400                     f"client_sent={client_sent}; "
401                     f"client_received={client_received}; "
402                     f"{l7_data}"
403                 )
404
405
406 def main():
407     """Main function for the traffic generator using T-rex.
408
409     It verifies the given command line arguments and runs "simple_burst"
410     function.
411     """
412     parser = argparse.ArgumentParser()
413     parser.add_argument(
414         u"-p", u"--profile", required=True, type=str,
415         help=u"Python traffic profile."
416     )
417     parser.add_argument(
418         u"-d", u"--duration", required=True, type=float,
419         help=u"Duration of the whole traffic run, including overheads."
420     )
421     parser.add_argument(
422         u"-s", u"--frame_size", required=True,
423         help=u"Size of a Frame without padding and IPG."
424     )
425     parser.add_argument(
426         u"--n_data_frames", type=int, default=5,
427         help=u"Use this many data frames per transaction and direction (TPUT)."
428     )
429     parser.add_argument(
430         u"-m", u"--multiplier", required=True, type=float,
431         help=u"Multiplier of profile CPS."
432     )
433     parser.add_argument(
434         u"--port_0", required=True, type=int,
435         help=u"Port 0 on the traffic generator."
436     )
437     parser.add_argument(
438         u"--port_1", required=True, type=int,
439         help=u"Port 1 on the traffic generator."
440     )
441     parser.add_argument(
442         u"--async_start", action=u"store_true", default=False,
443         help=u"Non-blocking call of the script."
444     )
445     parser.add_argument(
446         u"--latency", action=u"store_true", default=False,
447         help=u"Add latency stream."
448     )
449     parser.add_argument(
450         u"--traffic_directions", type=int, default=2,
451         help=u"Send bi- (2) or uni- (1) directional traffic."
452     )
453     parser.add_argument(
454         u"--delay", required=True, type=float, default=0.0,
455         help=u"Allowed time overhead, sleep time is increased by this [s]."
456     )
457
458     args = parser.parse_args()
459
460     try:
461         framesize = int(args.frame_size)
462     except ValueError:
463         framesize = args.frame_size
464
465     simple_burst(
466         profile_file=args.profile,
467         duration=args.duration,
468         framesize=framesize,
469         n_data_frames=args.n_data_frames,
470         multiplier=args.multiplier,
471         port_0=args.port_0,
472         port_1=args.port_1,
473         latency=args.latency,
474         async_start=args.async_start,
475         traffic_directions=args.traffic_directions,
476         delay=args.delay,
477     )
478
479
480 if __name__ == u"__main__":
481     main()