fix(core): T-Rex initialization
[csit.git] / resources / libraries / python / TrafficGenerator.py
1 # Copyright (c) 2022 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Performance testing traffic generator library."""
15
16 import math
17 import time
18
19 from robot.api import logger
20 from robot.libraries.BuiltIn import BuiltIn
21
22 from .Constants import Constants
23 from .CpuUtils import CpuUtils
24 from .DropRateSearch import DropRateSearch
25 from .MLRsearch.AbstractMeasurer import AbstractMeasurer
26 from .MLRsearch.MultipleLossRatioSearch import MultipleLossRatioSearch
27 from .MLRsearch.ReceiveRateMeasurement import ReceiveRateMeasurement
28 from .PLRsearch.PLRsearch import PLRsearch
29 from .OptionString import OptionString
30 from .ssh import exec_cmd_no_error, exec_cmd
31 from .topology import NodeType
32 from .topology import NodeSubTypeTG
33 from .topology import Topology
34
35 __all__ = [u"TGDropRateSearchImpl", u"TrafficGenerator", u"OptimizedSearch"]
36
37
38 def check_subtype(node):
39     """Return supported subtype of given node, or raise an exception.
40
41     Currently only one subtype is supported,
42     but we want our code to be ready for other ones.
43
44     :param node: Topology node to check. Can be None.
45     :type node: dict or NoneType
46     :returns: Subtype detected.
47     :rtype: NodeSubTypeTG
48     :raises RuntimeError: If node is not supported, message explains how.
49     """
50     if node.get(u"type") is None:
51         msg = u"Node type is not defined"
52     elif node[u"type"] != NodeType.TG:
53         msg = f"Node type is {node[u'type']!r}, not a TG"
54     elif node.get(u"subtype") is None:
55         msg = u"TG subtype is not defined"
56     elif node[u"subtype"] != NodeSubTypeTG.TREX:
57         msg = f"TG subtype {node[u'subtype']!r} is not supported"
58     else:
59         return NodeSubTypeTG.TREX
60     raise RuntimeError(msg)
61
62
63 class TGDropRateSearchImpl(DropRateSearch):
64     """Drop Rate Search implementation."""
65
66     # def __init__(self):
67     #     super(TGDropRateSearchImpl, self).__init__()
68
69     def measure_loss(
70             self, rate, frame_size, loss_acceptance, loss_acceptance_type,
71             traffic_profile):
72         """Runs the traffic and evaluate the measured results.
73
74         :param rate: Offered traffic load.
75         :param frame_size: Size of frame.
76         :param loss_acceptance: Permitted drop ratio or frames count.
77         :param loss_acceptance_type: Type of permitted loss.
78         :param traffic_profile: Module name as a traffic profile identifier.
79             See GPL/traffic_profiles/trex for implemented modules.
80         :type rate: float
81         :type frame_size: str
82         :type loss_acceptance: float
83         :type loss_acceptance_type: LossAcceptanceType
84         :type traffic_profile: str
85         :returns: Drop threshold exceeded? (True/False)
86         :rtype: bool
87         :raises NotImplementedError: If TG is not supported.
88         :raises RuntimeError: If TG is not specified.
89         """
90         # we need instance of TrafficGenerator instantiated by Robot Framework
91         # to be able to use trex_stl-*()
92         tg_instance = BuiltIn().get_library_instance(
93             u"resources.libraries.python.TrafficGenerator"
94         )
95         subtype = check_subtype(tg_instance.node)
96         if subtype == NodeSubTypeTG.TREX:
97             unit_rate = str(rate) + self.get_rate_type_str()
98             tg_instance.trex_stl_start_remote_exec(
99                 self.get_duration(), unit_rate, frame_size, traffic_profile
100             )
101             loss = tg_instance.get_loss()
102             sent = tg_instance.get_sent()
103             if self.loss_acceptance_type_is_percentage():
104                 loss = (float(loss) / float(sent)) * 100
105             logger.trace(
106                 f"comparing: {loss} < {loss_acceptance} {loss_acceptance_type}"
107             )
108             return float(loss) <= float(loss_acceptance)
109         return False
110
111     def get_latency(self):
112         """Returns min/avg/max latency.
113
114         :returns: Latency stats.
115         :rtype: list
116         """
117         tg_instance = BuiltIn().get_library_instance(
118             u"resources.libraries.python.TrafficGenerator"
119         )
120         return tg_instance.get_latency_int()
121
122
123 class TrexMode:
124     """Defines mode of T-Rex traffic generator."""
125     # Advanced stateful mode
126     ASTF = u"ASTF"
127     # Stateless mode
128     STL = u"STL"
129
130
131 # TODO: Pylint says too-many-instance-attributes.
132 class TrafficGenerator(AbstractMeasurer):
133     """Traffic Generator."""
134
135     # TODO: Remove "trex" from lines which could work with other TGs.
136
137     # Use one instance of TrafficGenerator for all tests in test suite
138     ROBOT_LIBRARY_SCOPE = u"TEST SUITE"
139
140     def __init__(self):
141         # TODO: Separate into few dataclasses/dicts.
142         #       Pylint dislikes large unstructured state, and it is right.
143         self._node = None
144         self._mode = None
145         # TG interface order mapping
146         self._ifaces_reordered = False
147         # Result holding fields, to be removed.
148         self._result = None
149         self._loss = None
150         self._sent = None
151         self._latency = None
152         self._received = None
153         self._approximated_rate = None
154         self._approximated_duration = None
155         self._l7_data = None
156         # Measurement input fields, needed for async stop result.
157         self._start_time = None
158         self._stop_time = None
159         self._rate = None
160         self._target_duration = None
161         self._duration = None
162         # Other input parameters, not knowable from measure() signature.
163         self.frame_size = None
164         self.traffic_profile = None
165         self.traffic_directions = None
166         self.negative_loss = None
167         self.use_latency = None
168         self.ppta = None
169         self.resetter = None
170         self.transaction_scale = None
171         self.transaction_duration = None
172         self.sleep_till_duration = None
173         self.transaction_type = None
174         self.duration_limit = None
175         self.ramp_up_start = None
176         self.ramp_up_stop = None
177         self.ramp_up_rate = None
178         self.ramp_up_duration = None
179         self.state_timeout = None
180         # Transient data needed for async measurements.
181         self._xstats = (None, None)
182         # TODO: Rename "xstats" to something opaque, so T-Rex is not privileged?
183
184     @property
185     def node(self):
186         """Getter.
187
188         :returns: Traffic generator node.
189         :rtype: dict
190         """
191         return self._node
192
193     def get_loss(self):
194         """Return number of lost packets.
195
196         :returns: Number of lost packets.
197         :rtype: str
198         """
199         return self._loss
200
201     def get_sent(self):
202         """Return number of sent packets.
203
204         :returns: Number of sent packets.
205         :rtype: str
206         """
207         return self._sent
208
209     def get_received(self):
210         """Return number of received packets.
211
212         :returns: Number of received packets.
213         :rtype: str
214         """
215         return self._received
216
217     def get_latency_int(self):
218         """Return rounded min/avg/max latency.
219
220         :returns: Latency stats.
221         :rtype: list
222         """
223         return self._latency
224
225     def get_approximated_rate(self):
226         """Return approximated rate computed as ratio of transmitted packets
227         over duration of trial.
228
229         :returns: Approximated rate.
230         :rtype: str
231         """
232         return self._approximated_rate
233
234     def get_l7_data(self):
235         """Return L7 data.
236
237         :returns: Number of received packets.
238         :rtype: dict
239         """
240         return self._l7_data
241
242     def check_mode(self, expected_mode):
243         """Check TG mode.
244
245         :param expected_mode: Expected traffic generator mode.
246         :type expected_mode: object
247         :raises RuntimeError: In case of unexpected TG mode.
248         """
249         if self._mode == expected_mode:
250             return
251         raise RuntimeError(
252             f"{self._node[u'subtype']} not running in {expected_mode} mode!"
253         )
254
255     @staticmethod
256     def get_tg_type(tg_node):
257         """Log and return the installed traffic generator type.
258
259         :param tg_node: Node from topology file.
260         :type tg_node: dict
261         :returns: Traffic generator type string.
262         :rtype: str
263         :raises RuntimeError: If command returns nonzero return code.
264         """
265         return str(check_subtype(tg_node))
266
267     @staticmethod
268     def get_tg_version(tg_node):
269         """Log and return the installed traffic generator version.
270
271         :param tg_node: Node from topology file.
272         :type tg_node: dict
273         :returns: Traffic generator version string.
274         :rtype: str
275         :raises RuntimeError: If command returns nonzero return code.
276         """
277         subtype = check_subtype(tg_node)
278         if subtype == NodeSubTypeTG.TREX:
279             command = f"cat {Constants.TREX_INSTALL_DIR}/VERSION"
280             message = u"Get T-Rex version failed!"
281             stdout, _ = exec_cmd_no_error(tg_node, command, message=message)
282             return stdout.strip()
283         else:
284             return "none"
285
286     # TODO: pylint disable=too-many-locals.
287     def initialize_traffic_generator(
288             self, tg_node, tg_if1, tg_if2, tg_if1_adj_node, tg_if1_adj_if,
289             tg_if2_adj_node, tg_if2_adj_if, osi_layer, tg_if1_dst_mac=None,
290             tg_if2_dst_mac=None):
291         """TG initialization.
292
293         TODO: Document why do we need (and how do we use) _ifaces_reordered.
294
295         :param tg_node: Traffic generator node.
296         :param tg_if1: TG - name of first interface.
297         :param tg_if2: TG - name of second interface.
298         :param tg_if1_adj_node: TG if1 adjecent node.
299         :param tg_if1_adj_if: TG if1 adjecent interface.
300         :param tg_if2_adj_node: TG if2 adjecent node.
301         :param tg_if2_adj_if: TG if2 adjecent interface.
302         :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
303         :param tg_if1_dst_mac: Interface 1 destination MAC address.
304         :param tg_if2_dst_mac: Interface 2 destination MAC address.
305         :type tg_node: dict
306         :type tg_if1: str
307         :type tg_if2: str
308         :type tg_if1_adj_node: dict
309         :type tg_if1_adj_if: str
310         :type tg_if2_adj_node: dict
311         :type tg_if2_adj_if: str
312         :type osi_layer: str
313         :type tg_if1_dst_mac: str
314         :type tg_if2_dst_mac: str
315         :returns: nothing
316         :raises RuntimeError: In case of issue during initialization.
317         """
318         subtype = check_subtype(tg_node)
319         if subtype == NodeSubTypeTG.TREX:
320             self._node = tg_node
321             self._mode = TrexMode.ASTF if osi_layer == u"L7" else TrexMode.STL
322             if1 = dict()
323             if2 = dict()
324             if1[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if1)
325             if2[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if2)
326             if1[u"addr"] = Topology().get_interface_mac(self._node, tg_if1)
327             if2[u"addr"] = Topology().get_interface_mac(self._node, tg_if2)
328
329             if osi_layer == u"L2":
330                 if1[u"adj_addr"] = if2[u"addr"]
331                 if2[u"adj_addr"] = if1[u"addr"]
332             elif osi_layer in (u"L3", u"L7"):
333                 if1[u"adj_addr"] = Topology().get_interface_mac(
334                     tg_if1_adj_node, tg_if1_adj_if
335                 )
336                 if2[u"adj_addr"] = Topology().get_interface_mac(
337                     tg_if2_adj_node, tg_if2_adj_if
338                 )
339             else:
340                 raise ValueError(u"Unknown OSI layer!")
341
342             # in case of switched environment we can override MAC addresses
343             if tg_if1_dst_mac is not None and tg_if2_dst_mac is not None:
344                 if1[u"adj_addr"] = tg_if1_dst_mac
345                 if2[u"adj_addr"] = tg_if2_dst_mac
346
347             if min(if1[u"pci"], if2[u"pci"]) != if1[u"pci"]:
348                 if1, if2 = if2, if1
349                 self._ifaces_reordered = True
350
351             master_thread_id, latency_thread_id, socket, threads = \
352                 CpuUtils.get_affinity_trex(
353                     self._node, tg_if1, tg_if2,
354                     tg_dtc=Constants.TREX_CORE_COUNT)
355
356             if osi_layer in (u"L2", u"L3", u"L7"):
357                 exec_cmd_no_error(
358                     self._node,
359                     f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
360                     f"- version: 2\n"
361                     f"  c: {len(threads)}\n"
362                     f"  limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
363                     f"  interfaces: [\"{if1[u'pci']}\",\"{if2[u'pci']}\"]\n"
364                     f"  port_info:\n"
365                     f"      - dest_mac: \'{if1[u'adj_addr']}\'\n"
366                     f"        src_mac: \'{if1[u'addr']}\'\n"
367                     f"      - dest_mac: \'{if2[u'adj_addr']}\'\n"
368                     f"        src_mac: \'{if2[u'addr']}\'\n"
369                     f"  platform :\n"
370                     f"      master_thread_id: {master_thread_id}\n"
371                     f"      latency_thread_id: {latency_thread_id}\n"
372                     f"      dual_if:\n"
373                     f"          - socket: {socket}\n"
374                     f"            threads: {threads}\n"
375                     f"EOF'",
376                     sudo=True, message=u"T-Rex config generation!"
377                 )
378
379                 if Constants.TREX_RX_DESCRIPTORS_COUNT != 0:
380                     exec_cmd_no_error(
381                         self._node,
382                         f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
383                         f"  rx_desc: {Constants.TREX_RX_DESCRIPTORS_COUNT}\n"
384                         f"EOF'",
385                         sudo=True, message=u"T-Rex rx_desc modification!"
386                     )
387
388                 if Constants.TREX_TX_DESCRIPTORS_COUNT != 0:
389                     exec_cmd_no_error(
390                         self._node,
391                         f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
392                         f"  tx_desc: {Constants.TREX_TX_DESCRIPTORS_COUNT}\n"
393                         f"EOF'",
394                         sudo=True, message=u"T-Rex tx_desc modification!"
395                     )
396             else:
397                 raise ValueError(u"Unknown OSI layer!")
398
399             TrafficGenerator.startup_trex(
400                 self._node, osi_layer, subtype=subtype
401             )
402
403     @staticmethod
404     def startup_trex(tg_node, osi_layer, subtype=None):
405         """Startup sequence for the TRex traffic generator.
406
407         :param tg_node: Traffic generator node.
408         :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
409         :param subtype: Traffic generator sub-type.
410         :type tg_node: dict
411         :type osi_layer: str
412         :type subtype: NodeSubTypeTG
413         :raises RuntimeError: If T-Rex startup failed.
414         :raises ValueError: If OSI layer is not supported.
415         """
416         if not subtype:
417             subtype = check_subtype(tg_node)
418         if subtype == NodeSubTypeTG.TREX:
419             for _ in range(0, 3):
420                 # Kill TRex only if it is already running.
421                 cmd = u"sh -c \"pgrep t-rex && pkill t-rex && sleep 3 || true\""
422                 exec_cmd_no_error(
423                     tg_node, cmd, sudo=True, message=u"Kill TRex failed!"
424                 )
425
426                 # Prepare interfaces for TRex.
427                 mlx_ports = u""
428                 mlx_driver = u""
429                 itl_ports = u""
430                 for port in tg_node[u"interfaces"].values():
431                     if u"Mellanox" in port.get(u"model"):
432                         mlx_ports += f" {port.get(u'pci_address')}"
433                         mlx_driver = port.get(u"driver")
434                     if u"Intel" in port.get(u"model"):
435                         itl_ports += f" {port.get(u'pci_address')}"
436
437                 if itl_ports:
438                     cmd = (
439                         f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && ",
440                         f"./dpdk_nic_bind.py -u {itl_ports} || ",
441                         f"true\""
442                     )
443                     exec_cmd_no_error(
444                         tg_node, cmd, sudo=True,
445                         message=u"Unbind PCI ports from driver failed!"
446                     )
447                 if mlx_ports:
448                     cmd = (
449                         f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && ",
450                         f"./dpdk_nic_bind.py -b {mlx_driver} {mlx_ports} || ",
451                         f"true\""
452                     )
453                     exec_cmd_no_error(
454                         tg_node, cmd, sudo=True,
455                         message=u"Bind PCI ports from driver failed!"
456                     )
457
458                 # Start TRex.
459                 cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
460                 trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
461                 trex_cmd.add(u"-i")
462                 trex_cmd.add(u"--prefix $(hostname)")
463                 trex_cmd.add(u"--hdrh")
464                 trex_cmd.add(u"--no-scapy-server")
465                 trex_cmd.add_if(u"--astf", osi_layer == u"L7")
466                 # OptionString does not create double space if extra is empty.
467                 trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
468                 inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
469                 cmd = f"sh -c \"{inner_command}\" > /dev/null"
470                 try:
471                     exec_cmd_no_error(tg_node, cmd, sudo=True)
472                 except RuntimeError:
473                     cmd = u"sh -c \"cat /tmp/trex.log\""
474                     exec_cmd_no_error(
475                         tg_node, cmd, sudo=True,
476                         message=u"Get TRex logs failed!"
477                     )
478                     raise RuntimeError(u"Start TRex failed!")
479
480                 # Test T-Rex API responsiveness.
481                 cmd = f"python3 {Constants.REMOTE_FW_DIR}/GPL/tools/trex/"
482                 if osi_layer in (u"L2", u"L3"):
483                     cmd += u"trex_stl_assert.py"
484                 elif osi_layer == u"L7":
485                     cmd += u"trex_astf_assert.py"
486                 else:
487                     raise ValueError(u"Unknown OSI layer!")
488                 try:
489                     exec_cmd_no_error(
490                         tg_node, cmd, sudo=True,
491                         message=u"T-Rex API is not responding!", retries=20
492                     )
493                 except RuntimeError:
494                     continue
495                 return
496             # After max retries TRex is still not responding to API critical
497             # error occurred.
498             exec_cmd(tg_node, u"cat /tmp/trex.log", sudo=True)
499             raise RuntimeError(u"Start T-Rex failed after multiple retries!")
500
501     @staticmethod
502     def is_trex_running(node):
503         """Check if T-Rex is running using pidof.
504
505         :param node: Traffic generator node.
506         :type node: dict
507         :returns: True if T-Rex is running otherwise False.
508         :rtype: bool
509         """
510         ret, _, _ = exec_cmd(node, u"pgrep t-rex", sudo=True)
511         return bool(int(ret) == 0)
512
513     @staticmethod
514     def teardown_traffic_generator(node):
515         """TG teardown.
516
517         :param node: Traffic generator node.
518         :type node: dict
519         :returns: nothing
520         :raises RuntimeError: If node type is not a TG,
521             or if T-Rex teardown fails.
522         """
523         subtype = check_subtype(node)
524         if subtype == NodeSubTypeTG.TREX:
525             exec_cmd_no_error(
526                 node,
527                 u"sh -c "
528                 u"\"if pgrep t-rex; then sudo pkill t-rex && sleep 3; fi\"",
529                 sudo=False,
530                 message=u"T-Rex kill failed!"
531             )
532
533     def trex_astf_stop_remote_exec(self, node):
534         """Execute T-Rex ASTF script on remote node over ssh to stop running
535         traffic.
536
537         Internal state is updated with measurement results.
538
539         :param node: T-Rex generator node.
540         :type node: dict
541         :raises RuntimeError: If stop traffic script fails.
542         """
543         command_line = OptionString().add(u"python3")
544         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
545         command_line.add(f"'{dirname}/trex_astf_stop.py'")
546         command_line.change_prefix(u"--")
547         for index, value in enumerate(self._xstats):
548             if value is not None:
549                 value = value.replace(u"'", u"\"")
550                 command_line.add_equals(f"xstat{index}", f"'{value}'")
551         stdout, _ = exec_cmd_no_error(
552             node, command_line,
553             message=u"T-Rex ASTF runtime error!"
554         )
555         self._parse_traffic_results(stdout)
556
557     def trex_stl_stop_remote_exec(self, node):
558         """Execute T-Rex STL script on remote node over ssh to stop running
559         traffic.
560
561         Internal state is updated with measurement results.
562
563         :param node: T-Rex generator node.
564         :type node: dict
565         :raises RuntimeError: If stop traffic script fails.
566         """
567         command_line = OptionString().add(u"python3")
568         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
569         command_line.add(f"'{dirname}/trex_stl_stop.py'")
570         command_line.change_prefix(u"--")
571         for index, value in enumerate(self._xstats):
572             if value is not None:
573                 value = value.replace(u"'", u"\"")
574                 command_line.add_equals(f"xstat{index}", f"'{value}'")
575         stdout, _ = exec_cmd_no_error(
576             node, command_line,
577             message=u"T-Rex STL runtime error!"
578         )
579         self._parse_traffic_results(stdout)
580
581     def stop_traffic_on_tg(self):
582         """Stop all traffic on TG.
583
584         :returns: Structure containing the result of the measurement.
585         :rtype: ReceiveRateMeasurement
586         :raises ValueError: If TG traffic profile is not supported.
587         """
588         subtype = check_subtype(self._node)
589         if subtype != NodeSubTypeTG.TREX:
590             raise ValueError(f"Unsupported TG subtype: {subtype!r}")
591         if u"trex-astf" in self.traffic_profile:
592             self.trex_astf_stop_remote_exec(self._node)
593         elif u"trex-stl" in self.traffic_profile:
594             self.trex_stl_stop_remote_exec(self._node)
595         else:
596             raise ValueError(u"Unsupported T-Rex traffic profile!")
597         self._stop_time = time.monotonic()
598
599         return self._get_measurement_result()
600
601     def _compute_duration(self, duration, multiplier):
602         """Compute duration for profile driver.
603
604         The final result is influenced by transaction scale and duration limit.
605         It is assumed a higher level function has already set those to self.
606         The duration argument is the target value from search point of view,
607         before the overrides are applied here.
608
609         Minus one (signalling async traffic start) is kept.
610
611         Completeness flag is also included. Duration limited or async trials
612         are not considered complete for ramp-up purposes.
613
614         :param duration: Time expressed in seconds for how long to send traffic.
615         :param multiplier: Traffic rate in transactions per second.
616         :type duration: float
617         :type multiplier: float
618         :returns: New duration and whether it was a complete ramp-up candidate.
619         :rtype: float, bool
620         """
621         if duration < 0.0:
622             # Keep the async -1.
623             return duration, False
624         computed_duration = duration
625         if self.transaction_scale:
626             computed_duration = self.transaction_scale / multiplier
627             # Log the computed duration,
628             # so we can compare with what telemetry suggests
629             # the real duration was.
630             logger.debug(f"Expected duration {computed_duration}")
631         if not self.duration_limit:
632             return computed_duration, True
633         limited_duration = min(computed_duration, self.duration_limit)
634         return limited_duration, (limited_duration == computed_duration)
635
636     def trex_astf_start_remote_exec(
637             self, duration, multiplier, async_call=False):
638         """Execute T-Rex ASTF script on remote node over ssh to start running
639         traffic.
640
641         In sync mode, measurement results are stored internally.
642         In async mode, initial data including xstats are stored internally.
643
644         This method contains the logic to compute duration as maximum time
645         if transaction_scale is nonzero.
646         The transaction_scale argument defines (limits) how many transactions
647         will be started in total. As that amount of transaction can take
648         considerable time (sometimes due to explicit delays in the profile),
649         the real time a trial needs to finish is computed here. For now,
650         in that case the duration argument is ignored, assuming it comes
651         from ASTF-unaware search algorithm. The overall time a single
652         transaction needs is given in parameter transaction_duration,
653         it includes both explicit delays and implicit time it takes
654         to transfer data (or whatever the transaction does).
655
656         Currently it is observed TRex does not start the ASTF traffic
657         immediately, an ad-hoc constant is added to the computed duration
658         to compensate for that.
659
660         If transaction_scale is zero, duration is not recomputed.
661         It is assumed the subsequent result parsing gets the real duration
662         if the traffic stops sooner for any reason.
663
664         Currently, it is assumed traffic profile defines a single transaction.
665         To avoid heavy logic here, the input rate is expected to be in
666         transactions per second, as that directly translates to TRex multiplier,
667         (assuming the profile does not override the default cps value of one).
668
669         :param duration: Time expressed in seconds for how long to send traffic.
670         :param multiplier: Traffic rate in transactions per second.
671         :param async_call: If enabled then don't wait for all incoming traffic.
672         :type duration: float
673         :type multiplier: int
674         :type async_call: bool
675         :raises RuntimeError: In case of T-Rex driver issue.
676         """
677         self.check_mode(TrexMode.ASTF)
678         p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
679         if not isinstance(duration, (float, int)):
680             duration = float(duration)
681
682         # TODO: Refactor the code so duration is computed only once,
683         # and both the initial and the computed durations are logged.
684         computed_duration, _ = self._compute_duration(duration, multiplier)
685
686         command_line = OptionString().add(u"python3")
687         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
688         command_line.add(f"'{dirname}/trex_astf_profile.py'")
689         command_line.change_prefix(u"--")
690         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
691         command_line.add_with_value(
692             u"profile", f"'{dirname}/{self.traffic_profile}.py'"
693         )
694         command_line.add_with_value(u"duration", f"{computed_duration!r}")
695         command_line.add_with_value(u"frame_size", self.frame_size)
696         command_line.add_with_value(
697             u"n_data_frames", Constants.ASTF_N_DATA_FRAMES
698         )
699         command_line.add_with_value(u"multiplier", multiplier)
700         command_line.add_with_value(u"port_0", p_0)
701         command_line.add_with_value(u"port_1", p_1)
702         command_line.add_with_value(
703             u"traffic_directions", self.traffic_directions
704         )
705         command_line.add_if(u"async_start", async_call)
706         command_line.add_if(u"latency", self.use_latency)
707         command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
708         command_line.add_with_value(
709             u"delay", Constants.PERF_TRIAL_ASTF_DELAY
710         )
711
712         self._start_time = time.monotonic()
713         self._rate = multiplier
714         stdout, _ = exec_cmd_no_error(
715             self._node, command_line, timeout=computed_duration + 10.0,
716             message=u"T-Rex ASTF runtime error!"
717         )
718
719         if async_call:
720             # no result
721             self._target_duration = None
722             self._duration = None
723             self._received = None
724             self._sent = None
725             self._loss = None
726             self._latency = None
727             xstats = [None, None]
728             self._l7_data = dict()
729             self._l7_data[u"client"] = dict()
730             self._l7_data[u"client"][u"active_flows"] = None
731             self._l7_data[u"client"][u"established_flows"] = None
732             self._l7_data[u"client"][u"traffic_duration"] = None
733             self._l7_data[u"server"] = dict()
734             self._l7_data[u"server"][u"active_flows"] = None
735             self._l7_data[u"server"][u"established_flows"] = None
736             self._l7_data[u"server"][u"traffic_duration"] = None
737             if u"udp" in self.traffic_profile:
738                 self._l7_data[u"client"][u"udp"] = dict()
739                 self._l7_data[u"client"][u"udp"][u"connects"] = None
740                 self._l7_data[u"client"][u"udp"][u"closed_flows"] = None
741                 self._l7_data[u"client"][u"udp"][u"err_cwf"] = None
742                 self._l7_data[u"server"][u"udp"] = dict()
743                 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = None
744                 self._l7_data[u"server"][u"udp"][u"closed_flows"] = None
745             elif u"tcp" in self.traffic_profile:
746                 self._l7_data[u"client"][u"tcp"] = dict()
747                 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = None
748                 self._l7_data[u"client"][u"tcp"][u"connects"] = None
749                 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = None
750                 self._l7_data[u"client"][u"tcp"][u"connattempt"] = None
751                 self._l7_data[u"server"][u"tcp"] = dict()
752                 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = None
753                 self._l7_data[u"server"][u"tcp"][u"connects"] = None
754                 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = None
755             else:
756                 logger.warn(u"Unsupported T-Rex ASTF traffic profile!")
757             index = 0
758             for line in stdout.splitlines():
759                 if f"Xstats snapshot {index}: " in line:
760                     xstats[index] = line[19:]
761                     index += 1
762                 if index == 2:
763                     break
764             self._xstats = tuple(xstats)
765         else:
766             self._target_duration = duration
767             self._duration = computed_duration
768             self._parse_traffic_results(stdout)
769
770     def trex_stl_start_remote_exec(self, duration, rate, async_call=False):
771         """Execute T-Rex STL script on remote node over ssh to start running
772         traffic.
773
774         In sync mode, measurement results are stored internally.
775         In async mode, initial data including xstats are stored internally.
776
777         Mode-unaware code (e.g. in search algorithms) works with transactions.
778         To keep the logic simple, multiplier is set to that value.
779         As bidirectional traffic profiles send packets in both directions,
780         they are treated as transactions with two packets (one per direction).
781
782         :param duration: Time expressed in seconds for how long to send traffic.
783         :param rate: Traffic rate in transactions per second.
784         :param async_call: If enabled then don't wait for all incoming traffic.
785         :type duration: float
786         :type rate: str
787         :type async_call: bool
788         :raises RuntimeError: In case of T-Rex driver issue.
789         """
790         self.check_mode(TrexMode.STL)
791         p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
792         if not isinstance(duration, (float, int)):
793             duration = float(duration)
794
795         # TODO: Refactor the code so duration is computed only once,
796         # and both the initial and the computed durations are logged.
797         duration, _ = self._compute_duration(duration=duration, multiplier=rate)
798
799         command_line = OptionString().add(u"python3")
800         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
801         command_line.add(f"'{dirname}/trex_stl_profile.py'")
802         command_line.change_prefix(u"--")
803         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
804         command_line.add_with_value(
805             u"profile", f"'{dirname}/{self.traffic_profile}.py'"
806         )
807         command_line.add_with_value(u"duration", f"{duration!r}")
808         command_line.add_with_value(u"frame_size", self.frame_size)
809         command_line.add_with_value(u"rate", f"{rate!r}")
810         command_line.add_with_value(u"port_0", p_0)
811         command_line.add_with_value(u"port_1", p_1)
812         command_line.add_with_value(
813             u"traffic_directions", self.traffic_directions
814         )
815         command_line.add_if(u"async_start", async_call)
816         command_line.add_if(u"latency", self.use_latency)
817         command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
818         command_line.add_with_value(u"delay", Constants.PERF_TRIAL_STL_DELAY)
819
820         # TODO: This is ugly. Handle parsing better.
821         self._start_time = time.monotonic()
822         self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
823         stdout, _ = exec_cmd_no_error(
824             self._node, command_line, timeout=int(duration) + 60,
825             message=u"T-Rex STL runtime error"
826         )
827
828         if async_call:
829             # no result
830             self._target_duration = None
831             self._duration = None
832             self._received = None
833             self._sent = None
834             self._loss = None
835             self._latency = None
836
837             xstats = [None, None]
838             index = 0
839             for line in stdout.splitlines():
840                 if f"Xstats snapshot {index}: " in line:
841                     xstats[index] = line[19:]
842                     index += 1
843                 if index == 2:
844                     break
845             self._xstats = tuple(xstats)
846         else:
847             self._target_duration = duration
848             self._duration = duration
849             self._parse_traffic_results(stdout)
850
851     def send_traffic_on_tg(
852             self,
853             duration,
854             rate,
855             frame_size,
856             traffic_profile,
857             async_call=False,
858             ppta=1,
859             traffic_directions=2,
860             transaction_duration=0.0,
861             transaction_scale=0,
862             transaction_type=u"packet",
863             duration_limit=0.0,
864             use_latency=False,
865             ramp_up_rate=None,
866             ramp_up_duration=None,
867             state_timeout=240.0,
868             ramp_up_only=False,
869         ):
870         """Send traffic from all configured interfaces on TG.
871
872         In async mode, xstats is stored internally,
873         to enable getting correct result when stopping the traffic.
874         In both modes, stdout is returned,
875         but _parse_traffic_results only works in sync output.
876
877         Note that traffic generator uses DPDK driver which might
878         reorder port numbers based on wiring and PCI numbering.
879         This method handles that, so argument values are invariant,
880         but you can see swapped valued in debug logs.
881
882         When transaction_scale is specified, the duration value is ignored
883         and the needed time is computed. For cases where this results in
884         to too long measurement (e.g. teardown trial with small rate),
885         duration_limit is applied (of non-zero), so the trial is stopped sooner.
886
887         Bidirectional STL profiles are treated as transactions with two packets.
888
889         The return value is None for async.
890
891         :param duration: Duration of test traffic generation in seconds.
892         :param rate: Traffic rate in transactions per second.
893         :param frame_size: Frame size (L2) in Bytes.
894         :param traffic_profile: Module name as a traffic profile identifier.
895             See GPL/traffic_profiles/trex for implemented modules.
896         :param async_call: Async mode.
897         :param ppta: Packets per transaction, aggregated over directions.
898             Needed for udp_pps which does not have a good transaction counter,
899             so we need to compute expected number of packets.
900             Default: 1.
901         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
902             Default: 2
903         :param transaction_duration: Total expected time to close transaction.
904         :param transaction_scale: Number of transactions to perform.
905             0 (default) means unlimited.
906         :param transaction_type: An identifier specifying which counters
907             and formulas to use when computing attempted and failed
908             transactions. Default: "packet".
909         :param duration_limit: Zero or maximum limit for computed (or given)
910             duration.
911         :param use_latency: Whether to measure latency during the trial.
912             Default: False.
913         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
914         :param ramp_up_duration: Duration of ramp-up trials [s].
915         :param state_timeout: Time of life of DUT state [s].
916         :param ramp_up_only: If true, do not perform main trial measurement.
917         :type duration: float
918         :type rate: float
919         :type frame_size: str
920         :type traffic_profile: str
921         :type async_call: bool
922         :type ppta: int
923         :type traffic_directions: int
924         :type transaction_duration: float
925         :type transaction_scale: int
926         :type transaction_type: str
927         :type duration_limit: float
928         :type use_latency: bool
929         :type ramp_up_rate: float
930         :type ramp_up_duration: float
931         :type state_timeout: float
932         :type ramp_up_only: bool
933         :returns: TG results.
934         :rtype: ReceiveRateMeasurement or None
935         :raises ValueError: If TG traffic profile is not supported.
936         """
937         self.set_rate_provider_defaults(
938             frame_size=frame_size,
939             traffic_profile=traffic_profile,
940             ppta=ppta,
941             traffic_directions=traffic_directions,
942             transaction_duration=transaction_duration,
943             transaction_scale=transaction_scale,
944             transaction_type=transaction_type,
945             duration_limit=duration_limit,
946             use_latency=use_latency,
947             ramp_up_rate=ramp_up_rate,
948             ramp_up_duration=ramp_up_duration,
949             state_timeout=state_timeout,
950         )
951         return self._send_traffic_on_tg_with_ramp_up(
952             duration=duration,
953             rate=rate,
954             async_call=async_call,
955             ramp_up_only=ramp_up_only,
956         )
957
958     def _send_traffic_on_tg_internal(
959             self, duration, rate, async_call=False):
960         """Send traffic from all configured interfaces on TG.
961
962         This is an internal function, it assumes set_rate_provider_defaults
963         has been called to remember most values.
964         The reason why need to remember various values is that
965         the traffic can be asynchronous, and parsing needs those values.
966         The reason why this is is a separate function from the one
967         which calls set_rate_provider_defaults is that some search algorithms
968         need to specify their own values, and we do not want the measure call
969         to overwrite them with defaults.
970
971         This function is used both for automated ramp-up trials
972         and for explicitly called trials.
973
974         :param duration: Duration of test traffic generation in seconds.
975         :param rate: Traffic rate in transactions per second.
976         :param async_call: Async mode.
977         :type duration: float
978         :type rate: float
979         :type async_call: bool
980         :returns: TG results.
981         :rtype: ReceiveRateMeasurement or None
982         :raises ValueError: If TG traffic profile is not supported.
983         """
984         subtype = check_subtype(self._node)
985         if subtype == NodeSubTypeTG.TREX:
986             if u"trex-astf" in self.traffic_profile:
987                 self.trex_astf_start_remote_exec(
988                     duration, float(rate), async_call
989                 )
990             elif u"trex-stl" in self.traffic_profile:
991                 unit_rate_str = str(rate) + u"pps"
992                 # TODO: Suport transaction_scale et al?
993                 self.trex_stl_start_remote_exec(
994                     duration, unit_rate_str, async_call
995                 )
996             else:
997                 raise ValueError(u"Unsupported T-Rex traffic profile!")
998
999         return None if async_call else self._get_measurement_result()
1000
1001     def _send_traffic_on_tg_with_ramp_up(
1002             self, duration, rate, async_call=False, ramp_up_only=False):
1003         """Send traffic from all interfaces on TG, maybe after ramp-up.
1004
1005         This is an internal function, it assumes set_rate_provider_defaults
1006         has been called to remember most values.
1007         The reason why need to remember various values is that
1008         the traffic can be asynchronous, and parsing needs those values.
1009         The reason why this is a separate function from the one
1010         which calls set_rate_provider_defaults is that some search algorithms
1011         need to specify their own values, and we do not want the measure call
1012         to overwrite them with defaults.
1013
1014         If ramp-up tracking is detected, a computation is performed,
1015         and if state timeout is near, trial at ramp-up rate and duration
1016         is inserted before the main trial measurement.
1017
1018         The ramp_up_only parameter forces a ramp-up without immediate
1019         trial measurement, which is useful in case self remembers
1020         a previous ramp-up trial that belongs to a different test (phase).
1021
1022         Return None if trial is async or ramp-up only.
1023
1024         :param duration: Duration of test traffic generation in seconds.
1025         :param rate: Traffic rate in transactions per second.
1026         :param async_call: Async mode.
1027         :param ramp_up_only: If true, do not perform main trial measurement.
1028         :type duration: float
1029         :type rate: float
1030         :type async_call: bool
1031         :type ramp_up_only: bool
1032         :returns: TG results.
1033         :rtype: ReceiveRateMeasurement or None
1034         :raises ValueError: If TG traffic profile is not supported.
1035         """
1036         complete = False
1037         if self.ramp_up_rate:
1038             # Figure out whether we need to insert a ramp-up trial.
1039             # TODO: Give up on async_call=True?
1040             if ramp_up_only or self.ramp_up_start is None:
1041                 # We never ramped up yet (at least not in this test case).
1042                 ramp_up_needed = True
1043             else:
1044                 # We ramped up before, but maybe it was too long ago.
1045                 # Adding a constant overhead to be safe.
1046                 time_now = time.monotonic() + 1.0
1047                 computed_duration, complete = self._compute_duration(
1048                     duration=duration,
1049                     multiplier=rate,
1050                 )
1051                 # There are two conditions for inserting ramp-up.
1052                 # If early sessions are expiring already,
1053                 # or if late sessions are to expire before measurement is over.
1054                 ramp_up_start_delay = time_now - self.ramp_up_start
1055                 ramp_up_stop_delay = time_now - self.ramp_up_stop
1056                 ramp_up_stop_delay += computed_duration
1057                 bigger_delay = max(ramp_up_start_delay, ramp_up_stop_delay)
1058                 # Final boolean decision.
1059                 ramp_up_needed = (bigger_delay >= self.state_timeout)
1060             if ramp_up_needed:
1061                 logger.debug(
1062                     u"State may time out during next real trial, "
1063                     u"inserting a ramp-up trial."
1064                 )
1065                 self.ramp_up_start = time.monotonic()
1066                 self._send_traffic_on_tg_internal(
1067                     duration=self.ramp_up_duration,
1068                     rate=self.ramp_up_rate,
1069                     async_call=async_call,
1070                 )
1071                 self.ramp_up_stop = time.monotonic()
1072                 logger.debug(u"Ramp-up done.")
1073             else:
1074                 logger.debug(
1075                     u"State will probably not time out during next real trial, "
1076                     u"no ramp-up trial needed just yet."
1077                 )
1078         if ramp_up_only:
1079             return None
1080         trial_start = time.monotonic()
1081         result = self._send_traffic_on_tg_internal(
1082             duration=duration,
1083             rate=rate,
1084             async_call=async_call,
1085         )
1086         trial_end = time.monotonic()
1087         if self.ramp_up_rate:
1088             # Optimization: No loss acts as a good ramp-up, if it was complete.
1089             if complete and result is not None and result.loss_count == 0:
1090                 logger.debug(u"Good trial acts as a ramp-up")
1091                 self.ramp_up_start = trial_start
1092                 self.ramp_up_stop = trial_end
1093             else:
1094                 logger.debug(u"Loss or incomplete, does not act as a ramp-up.")
1095         return result
1096
1097     def no_traffic_loss_occurred(self):
1098         """Fail if loss occurred in traffic run.
1099
1100         :returns: nothing
1101         :raises Exception: If loss occured.
1102         """
1103         if self._loss is None:
1104             raise RuntimeError(u"The traffic generation has not been issued")
1105         if self._loss != u"0":
1106             raise RuntimeError(f"Traffic loss occurred: {self._loss}")
1107
1108     def fail_if_no_traffic_forwarded(self):
1109         """Fail if no traffic forwarded.
1110
1111         TODO: Check number of passed transactions instead.
1112
1113         :returns: nothing
1114         :raises Exception: If no traffic forwarded.
1115         """
1116         if self._received is None:
1117             raise RuntimeError(u"The traffic generation has not been issued")
1118         if self._received == 0:
1119             raise RuntimeError(u"No traffic forwarded")
1120
1121     def partial_traffic_loss_accepted(
1122             self, loss_acceptance, loss_acceptance_type):
1123         """Fail if loss is higher then accepted in traffic run.
1124
1125         :param loss_acceptance: Permitted drop ratio or frames count.
1126         :param loss_acceptance_type: Type of permitted loss.
1127         :type loss_acceptance: float
1128         :type loss_acceptance_type: LossAcceptanceType
1129         :returns: nothing
1130         :raises Exception: If loss is above acceptance criteria.
1131         """
1132         if self._loss is None:
1133             raise Exception(u"The traffic generation has not been issued")
1134
1135         if loss_acceptance_type == u"percentage":
1136             loss = (float(self._loss) / float(self._sent)) * 100
1137         elif loss_acceptance_type == u"frames":
1138             loss = float(self._loss)
1139         else:
1140             raise Exception(u"Loss acceptance type not supported")
1141
1142         if loss > float(loss_acceptance):
1143             raise Exception(
1144                 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
1145             )
1146
1147     def _parse_traffic_results(self, stdout):
1148         """Parse stdout of scripts into fields of self.
1149
1150         Block of code to reuse, by sync start, or stop after async.
1151
1152         :param stdout: Text containing the standard output.
1153         :type stdout: str
1154         """
1155         subtype = check_subtype(self._node)
1156         if subtype == NodeSubTypeTG.TREX:
1157             # Last line from console output
1158             line = stdout.splitlines()[-1]
1159             results = line.split(u";")
1160             if results[-1] in (u" ", u""):
1161                 results.pop(-1)
1162             self._result = dict()
1163             for result in results:
1164                 key, value = result.split(u"=", maxsplit=1)
1165                 self._result[key.strip()] = value
1166             logger.info(f"TrafficGen results:\n{self._result}")
1167             self._received = int(self._result.get(u"total_received"), 0)
1168             self._sent = int(self._result.get(u"total_sent", 0))
1169             self._loss = int(self._result.get(u"frame_loss", 0))
1170             self._approximated_duration = \
1171                 self._result.get(u"approximated_duration", 0.0)
1172             if u"manual" not in str(self._approximated_duration):
1173                 self._approximated_duration = float(self._approximated_duration)
1174             self._latency = list()
1175             self._latency.append(self._result.get(u"latency_stream_0(usec)"))
1176             self._latency.append(self._result.get(u"latency_stream_1(usec)"))
1177             if self._mode == TrexMode.ASTF:
1178                 self._l7_data = dict()
1179                 self._l7_data[u"client"] = dict()
1180                 self._l7_data[u"client"][u"sent"] = \
1181                     int(self._result.get(u"client_sent", 0))
1182                 self._l7_data[u"client"][u"received"] = \
1183                     int(self._result.get(u"client_received", 0))
1184                 self._l7_data[u"client"][u"active_flows"] = \
1185                     int(self._result.get(u"client_active_flows", 0))
1186                 self._l7_data[u"client"][u"established_flows"] = \
1187                     int(self._result.get(u"client_established_flows", 0))
1188                 self._l7_data[u"client"][u"traffic_duration"] = \
1189                     float(self._result.get(u"client_traffic_duration", 0.0))
1190                 self._l7_data[u"client"][u"err_rx_throttled"] = \
1191                     int(self._result.get(u"client_err_rx_throttled", 0))
1192                 self._l7_data[u"client"][u"err_c_nf_throttled"] = \
1193                     int(self._result.get(u"client_err_nf_throttled", 0))
1194                 self._l7_data[u"client"][u"err_flow_overflow"] = \
1195                     int(self._result.get(u"client_err_flow_overflow", 0))
1196                 self._l7_data[u"server"] = dict()
1197                 self._l7_data[u"server"][u"active_flows"] = \
1198                     int(self._result.get(u"server_active_flows", 0))
1199                 self._l7_data[u"server"][u"established_flows"] = \
1200                     int(self._result.get(u"server_established_flows", 0))
1201                 self._l7_data[u"server"][u"traffic_duration"] = \
1202                     float(self._result.get(u"server_traffic_duration", 0.0))
1203                 self._l7_data[u"server"][u"err_rx_throttled"] = \
1204                     int(self._result.get(u"client_err_rx_throttled", 0))
1205                 if u"udp" in self.traffic_profile:
1206                     self._l7_data[u"client"][u"udp"] = dict()
1207                     self._l7_data[u"client"][u"udp"][u"connects"] = \
1208                         int(self._result.get(u"client_udp_connects", 0))
1209                     self._l7_data[u"client"][u"udp"][u"closed_flows"] = \
1210                         int(self._result.get(u"client_udp_closed", 0))
1211                     self._l7_data[u"client"][u"udp"][u"tx_bytes"] = \
1212                         int(self._result.get(u"client_udp_tx_bytes", 0))
1213                     self._l7_data[u"client"][u"udp"][u"rx_bytes"] = \
1214                         int(self._result.get(u"client_udp_rx_bytes", 0))
1215                     self._l7_data[u"client"][u"udp"][u"tx_packets"] = \
1216                         int(self._result.get(u"client_udp_tx_packets", 0))
1217                     self._l7_data[u"client"][u"udp"][u"rx_packets"] = \
1218                         int(self._result.get(u"client_udp_rx_packets", 0))
1219                     self._l7_data[u"client"][u"udp"][u"keep_drops"] = \
1220                         int(self._result.get(u"client_udp_keep_drops", 0))
1221                     self._l7_data[u"client"][u"udp"][u"err_cwf"] = \
1222                         int(self._result.get(u"client_err_cwf", 0))
1223                     self._l7_data[u"server"][u"udp"] = dict()
1224                     self._l7_data[u"server"][u"udp"][u"accepted_flows"] = \
1225                         int(self._result.get(u"server_udp_accepts", 0))
1226                     self._l7_data[u"server"][u"udp"][u"closed_flows"] = \
1227                         int(self._result.get(u"server_udp_closed", 0))
1228                     self._l7_data[u"server"][u"udp"][u"tx_bytes"] = \
1229                         int(self._result.get(u"server_udp_tx_bytes", 0))
1230                     self._l7_data[u"server"][u"udp"][u"rx_bytes"] = \
1231                         int(self._result.get(u"server_udp_rx_bytes", 0))
1232                     self._l7_data[u"server"][u"udp"][u"tx_packets"] = \
1233                         int(self._result.get(u"server_udp_tx_packets", 0))
1234                     self._l7_data[u"server"][u"udp"][u"rx_packets"] = \
1235                         int(self._result.get(u"server_udp_rx_packets", 0))
1236                 elif u"tcp" in self.traffic_profile:
1237                     self._l7_data[u"client"][u"tcp"] = dict()
1238                     self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = \
1239                         int(self._result.get(u"client_tcp_connect_inits", 0))
1240                     self._l7_data[u"client"][u"tcp"][u"connects"] = \
1241                         int(self._result.get(u"client_tcp_connects", 0))
1242                     self._l7_data[u"client"][u"tcp"][u"closed_flows"] = \
1243                         int(self._result.get(u"client_tcp_closed", 0))
1244                     self._l7_data[u"client"][u"tcp"][u"connattempt"] = \
1245                         int(self._result.get(u"client_tcp_connattempt", 0))
1246                     self._l7_data[u"client"][u"tcp"][u"tx_bytes"] = \
1247                         int(self._result.get(u"client_tcp_tx_bytes", 0))
1248                     self._l7_data[u"client"][u"tcp"][u"rx_bytes"] = \
1249                         int(self._result.get(u"client_tcp_rx_bytes", 0))
1250                     self._l7_data[u"server"][u"tcp"] = dict()
1251                     self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = \
1252                         int(self._result.get(u"server_tcp_accepts", 0))
1253                     self._l7_data[u"server"][u"tcp"][u"connects"] = \
1254                         int(self._result.get(u"server_tcp_connects", 0))
1255                     self._l7_data[u"server"][u"tcp"][u"closed_flows"] = \
1256                         int(self._result.get(u"server_tcp_closed", 0))
1257                     self._l7_data[u"server"][u"tcp"][u"tx_bytes"] = \
1258                         int(self._result.get(u"server_tcp_tx_bytes", 0))
1259                     self._l7_data[u"server"][u"tcp"][u"rx_bytes"] = \
1260                         int(self._result.get(u"server_tcp_rx_bytes", 0))
1261
1262     def _get_measurement_result(self):
1263         """Return the result of last measurement as ReceiveRateMeasurement.
1264
1265         Separate function, as measurements can end either by time
1266         or by explicit call, this is the common block at the end.
1267
1268         The target_tr field of ReceiveRateMeasurement is in
1269         transactions per second. Transmit count and loss count units
1270         depend on the transaction type. Usually they are in transactions
1271         per second, or aggregated packets per second.
1272
1273         TODO: Fail on running or already reported measurement.
1274
1275         :returns: Structure containing the result of the measurement.
1276         :rtype: ReceiveRateMeasurement
1277         """
1278         try:
1279             # Client duration seems to include a setup period
1280             # where TRex does not send any packets yet.
1281             # Server duration does not include it.
1282             server_data = self._l7_data[u"server"]
1283             approximated_duration = float(server_data[u"traffic_duration"])
1284         except (KeyError, AttributeError, ValueError, TypeError):
1285             approximated_duration = None
1286         try:
1287             if not approximated_duration:
1288                 approximated_duration = float(self._approximated_duration)
1289         except ValueError:  # "manual"
1290             approximated_duration = None
1291         if not approximated_duration:
1292             if self._duration and self._duration > 0:
1293                 # Known recomputed or target duration.
1294                 approximated_duration = self._duration
1295             else:
1296                 # It was an explicit stop.
1297                 if not self._stop_time:
1298                     raise RuntimeError(u"Unable to determine duration.")
1299                 approximated_duration = self._stop_time - self._start_time
1300         target_duration = self._target_duration
1301         if not target_duration:
1302             target_duration = approximated_duration
1303         transmit_rate = self._rate
1304         unsent = 0
1305         if self.transaction_type == u"packet":
1306             partial_attempt_count = self._sent
1307             packet_rate = transmit_rate * self.ppta
1308             # We have a float. TRex way of rounding it is not obvious.
1309             # The biggest source of mismatch is Inter Stream Gap.
1310             # So the code tolerates 10 usec of missing packets.
1311             expected_attempt_count = (target_duration - 1e-5) * packet_rate
1312             expected_attempt_count = math.ceil(expected_attempt_count)
1313             # TRex can send more.
1314             expected_attempt_count = max(expected_attempt_count, self._sent)
1315             unsent = expected_attempt_count - self._sent
1316             pass_count = self._received
1317             fail_count = expected_attempt_count - pass_count
1318         elif self.transaction_type == u"udp_cps":
1319             if not self.transaction_scale:
1320                 raise RuntimeError(u"Add support for no-limit udp_cps.")
1321             partial_attempt_count = self._l7_data[u"client"][u"sent"]
1322             # We do not care whether TG is slow, it should have attempted all.
1323             expected_attempt_count = self.transaction_scale
1324             unsent = expected_attempt_count - partial_attempt_count
1325             pass_count = self._l7_data[u"client"][u"received"]
1326             fail_count = expected_attempt_count - pass_count
1327         elif self.transaction_type == u"tcp_cps":
1328             if not self.transaction_scale:
1329                 raise RuntimeError(u"Add support for no-limit tcp_cps.")
1330             ctca = self._l7_data[u"client"][u"tcp"][u"connattempt"]
1331             partial_attempt_count = ctca
1332             # We do not care whether TG is slow, it should have attempted all.
1333             expected_attempt_count = self.transaction_scale
1334             unsent = expected_attempt_count - partial_attempt_count
1335             # From TCP point of view, server/connects counts full connections,
1336             # but we are testing NAT session so client/connects counts that
1337             # (half connections from TCP point of view).
1338             pass_count = self._l7_data[u"client"][u"tcp"][u"connects"]
1339             fail_count = expected_attempt_count - pass_count
1340         elif self.transaction_type == u"udp_pps":
1341             if not self.transaction_scale:
1342                 raise RuntimeError(u"Add support for no-limit udp_pps.")
1343             partial_attempt_count = self._sent
1344             expected_attempt_count = self.transaction_scale * self.ppta
1345             unsent = expected_attempt_count - self._sent
1346             fail_count = self._loss + unsent
1347         elif self.transaction_type == u"tcp_pps":
1348             if not self.transaction_scale:
1349                 raise RuntimeError(u"Add support for no-limit tcp_pps.")
1350             partial_attempt_count = self._sent
1351             expected_attempt_count = self.transaction_scale * self.ppta
1352             # One loss-like scenario happens when TRex receives all packets
1353             # on L2 level, but is not fast enough to process them all
1354             # at L7 level, which leads to retransmissions.
1355             # Those manifest as opackets larger than expected.
1356             # A simple workaround is to add absolute difference.
1357             # Probability of retransmissions exactly cancelling
1358             # packets unsent due to duration stretching is quite low.
1359             unsent = abs(expected_attempt_count - self._sent)
1360             fail_count = self._loss + unsent
1361         else:
1362             raise RuntimeError(f"Unknown parsing {self.transaction_type!r}")
1363         if unsent and isinstance(self._approximated_duration, float):
1364             # Do not report unsent for "manual".
1365             logger.debug(f"Unsent packets/transactions: {unsent}")
1366         if fail_count < 0 and not self.negative_loss:
1367             fail_count = 0
1368         measurement = ReceiveRateMeasurement(
1369             duration=target_duration,
1370             target_tr=transmit_rate,
1371             transmit_count=expected_attempt_count,
1372             loss_count=fail_count,
1373             approximated_duration=approximated_duration,
1374             partial_transmit_count=partial_attempt_count,
1375         )
1376         measurement.latency = self.get_latency_int()
1377         return measurement
1378
1379     def measure(self, duration, transmit_rate):
1380         """Run trial measurement, parse and return results.
1381
1382         The input rate is for transactions. Stateles bidirectional traffic
1383         is understood as sequence of (asynchronous) transactions,
1384         two packets each.
1385
1386         The result units depend on test type, generally
1387         the count either transactions or packets (aggregated over directions).
1388
1389         Optionally, this method sleeps if measurement finished before
1390         the time specified as duration.
1391
1392         :param duration: Trial duration [s].
1393         :param transmit_rate: Target rate in transactions per second.
1394         :type duration: float
1395         :type transmit_rate: float
1396         :returns: Structure containing the result of the measurement.
1397         :rtype: ReceiveRateMeasurement
1398         :raises RuntimeError: If TG is not set or if node is not TG
1399             or if subtype is not specified.
1400         :raises NotImplementedError: If TG is not supported.
1401         """
1402         duration = float(duration)
1403         time_start = time.monotonic()
1404         time_stop = time_start + duration
1405         if self.resetter:
1406             self.resetter()
1407         result = self._send_traffic_on_tg_with_ramp_up(
1408             duration=duration,
1409             rate=transmit_rate,
1410             async_call=False,
1411         )
1412         logger.debug(f"trial measurement result: {result!r}")
1413         # In PLRsearch, computation needs the specified time to complete.
1414         if self.sleep_till_duration:
1415             sleeptime = time_stop - time.monotonic()
1416             if sleeptime > 0.0:
1417                 # TODO: Sometimes we have time to do additional trials here,
1418                 # adapt PLRsearch to accept all the results.
1419                 time.sleep(sleeptime)
1420         return result
1421
1422     def set_rate_provider_defaults(
1423             self,
1424             frame_size,
1425             traffic_profile,
1426             ppta=1,
1427             resetter=None,
1428             traffic_directions=2,
1429             transaction_duration=0.0,
1430             transaction_scale=0,
1431             transaction_type=u"packet",
1432             duration_limit=0.0,
1433             negative_loss=True,
1434             sleep_till_duration=False,
1435             use_latency=False,
1436             ramp_up_rate=None,
1437             ramp_up_duration=None,
1438             state_timeout=240.0,
1439         ):
1440         """Store values accessed by measure().
1441
1442         :param frame_size: Frame size identifier or value [B].
1443         :param traffic_profile: Module name as a traffic profile identifier.
1444             See GPL/traffic_profiles/trex for implemented modules.
1445         :param ppta: Packets per transaction, aggregated over directions.
1446             Needed for udp_pps which does not have a good transaction counter,
1447             so we need to compute expected number of packets.
1448             Default: 1.
1449         :param resetter: Callable to reset DUT state for repeated trials.
1450         :param traffic_directions: Traffic from packet counting point of view
1451             is bi- (2) or uni- (1) directional.
1452             Default: 2
1453         :param transaction_duration: Total expected time to close transaction.
1454         :param transaction_scale: Number of transactions to perform.
1455             0 (default) means unlimited.
1456         :param transaction_type: An identifier specifying which counters
1457             and formulas to use when computing attempted and failed
1458             transactions. Default: "packet".
1459             TODO: Does this also specify parsing for the measured duration?
1460         :param duration_limit: Zero or maximum limit for computed (or given)
1461             duration.
1462         :param negative_loss: If false, negative loss is reported as zero loss.
1463         :param sleep_till_duration: If true and measurement returned faster,
1464             sleep until it matches duration. Needed for PLRsearch.
1465         :param use_latency: Whether to measure latency during the trial.
1466             Default: False.
1467         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1468         :param ramp_up_duration: Duration of ramp-up trials [s].
1469         :param state_timeout: Time of life of DUT state [s].
1470         :type frame_size: str or int
1471         :type traffic_profile: str
1472         :type ppta: int
1473         :type resetter: Optional[Callable[[], None]]
1474         :type traffic_directions: int
1475         :type transaction_duration: float
1476         :type transaction_scale: int
1477         :type transaction_type: str
1478         :type duration_limit: float
1479         :type negative_loss: bool
1480         :type sleep_till_duration: bool
1481         :type use_latency: bool
1482         :type ramp_up_rate: float
1483         :type ramp_up_duration: float
1484         :type state_timeout: float
1485         """
1486         self.frame_size = frame_size
1487         self.traffic_profile = str(traffic_profile)
1488         self.resetter = resetter
1489         self.ppta = ppta
1490         self.traffic_directions = int(traffic_directions)
1491         self.transaction_duration = float(transaction_duration)
1492         self.transaction_scale = int(transaction_scale)
1493         self.transaction_type = str(transaction_type)
1494         self.duration_limit = float(duration_limit)
1495         self.negative_loss = bool(negative_loss)
1496         self.sleep_till_duration = bool(sleep_till_duration)
1497         self.use_latency = bool(use_latency)
1498         self.ramp_up_rate = float(ramp_up_rate)
1499         self.ramp_up_duration = float(ramp_up_duration)
1500         self.state_timeout = float(state_timeout)
1501
1502
1503 class OptimizedSearch:
1504     """Class to be imported as Robot Library, containing search keywords.
1505
1506     Aside of setting up measurer and forwarding arguments,
1507     the main business is to translate min/max rate from unidir to aggregated.
1508     """
1509
1510     @staticmethod
1511     def perform_optimized_ndrpdr_search(
1512             frame_size,
1513             traffic_profile,
1514             minimum_transmit_rate,
1515             maximum_transmit_rate,
1516             packet_loss_ratio=0.005,
1517             final_relative_width=0.005,
1518             final_trial_duration=30.0,
1519             initial_trial_duration=1.0,
1520             number_of_intermediate_phases=2,
1521             timeout=1200.0,
1522             ppta=1,
1523             resetter=None,
1524             traffic_directions=2,
1525             transaction_duration=0.0,
1526             transaction_scale=0,
1527             transaction_type=u"packet",
1528             use_latency=False,
1529             ramp_up_rate=None,
1530             ramp_up_duration=None,
1531             state_timeout=240.0,
1532             expansion_coefficient=4.0,
1533     ):
1534         """Setup initialized TG, perform optimized search, return intervals.
1535
1536         If transaction_scale is nonzero, all init and non-init trial durations
1537         are set to 1.0 (as they do not affect the real trial duration)
1538         and zero intermediate phases are used.
1539         This way no re-measurement happens.
1540         Warmup has to be handled via resetter or ramp-up mechanisms.
1541
1542         :param frame_size: Frame size identifier or value [B].
1543         :param traffic_profile: Module name as a traffic profile identifier.
1544             See GPL/traffic_profiles/trex for implemented modules.
1545         :param minimum_transmit_rate: Minimal load in transactions per second.
1546         :param maximum_transmit_rate: Maximal load in transactions per second.
1547         :param packet_loss_ratio: Ratio of packets lost, for PDR [1].
1548         :param final_relative_width: Final lower bound transmit rate
1549             cannot be more distant that this multiple of upper bound [1].
1550         :param final_trial_duration: Trial duration for the final phase [s].
1551         :param initial_trial_duration: Trial duration for the initial phase
1552             and also for the first intermediate phase [s].
1553         :param number_of_intermediate_phases: Number of intermediate phases
1554             to perform before the final phase [1].
1555         :param timeout: The search will fail itself when not finished
1556             before this overall time [s].
1557         :param ppta: Packets per transaction, aggregated over directions.
1558             Needed for udp_pps which does not have a good transaction counter,
1559             so we need to compute expected number of packets.
1560             Default: 1.
1561         :param resetter: Callable to reset DUT state for repeated trials.
1562         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1563             Default: 2
1564         :param transaction_duration: Total expected time to close transaction.
1565         :param transaction_scale: Number of transactions to perform.
1566             0 (default) means unlimited.
1567         :param transaction_type: An identifier specifying which counters
1568             and formulas to use when computing attempted and failed
1569             transactions. Default: "packet".
1570         :param use_latency: Whether to measure latency during the trial.
1571             Default: False.
1572         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1573         :param ramp_up_duration: Duration of ramp-up trials [s].
1574         :param state_timeout: Time of life of DUT state [s].
1575         :param expansion_coefficient: In external search multiply width by this.
1576         :type frame_size: str or int
1577         :type traffic_profile: str
1578         :type minimum_transmit_rate: float
1579         :type maximum_transmit_rate: float
1580         :type packet_loss_ratio: float
1581         :type final_relative_width: float
1582         :type final_trial_duration: float
1583         :type initial_trial_duration: float
1584         :type number_of_intermediate_phases: int
1585         :type timeout: float
1586         :type ppta: int
1587         :type resetter: Optional[Callable[[], None]]
1588         :type traffic_directions: int
1589         :type transaction_duration: float
1590         :type transaction_scale: int
1591         :type transaction_type: str
1592         :type use_latency: bool
1593         :type ramp_up_rate: float
1594         :type ramp_up_duration: float
1595         :type state_timeout: float
1596         :type expansion_coefficient: float
1597         :returns: Structure containing narrowed down NDR and PDR intervals
1598             and their measurements.
1599         :rtype: List[Receiverateinterval]
1600         :raises RuntimeError: If total duration is larger than timeout.
1601         """
1602         # we need instance of TrafficGenerator instantiated by Robot Framework
1603         # to be able to use trex_stl-*()
1604         tg_instance = BuiltIn().get_library_instance(
1605             u"resources.libraries.python.TrafficGenerator"
1606         )
1607         # Overrides for fixed transaction amount.
1608         # TODO: Move to robot code? We have two call sites, so this saves space,
1609         #       even though this is surprising for log readers.
1610         if transaction_scale:
1611             initial_trial_duration = 1.0
1612             final_trial_duration = 1.0
1613             number_of_intermediate_phases = 0
1614             timeout += transaction_scale * 3e-4
1615         tg_instance.set_rate_provider_defaults(
1616             frame_size=frame_size,
1617             traffic_profile=traffic_profile,
1618             sleep_till_duration=False,
1619             ppta=ppta,
1620             resetter=resetter,
1621             traffic_directions=traffic_directions,
1622             transaction_duration=transaction_duration,
1623             transaction_scale=transaction_scale,
1624             transaction_type=transaction_type,
1625             use_latency=use_latency,
1626             ramp_up_rate=ramp_up_rate,
1627             ramp_up_duration=ramp_up_duration,
1628             state_timeout=state_timeout,
1629         )
1630         algorithm = MultipleLossRatioSearch(
1631             measurer=tg_instance,
1632             final_trial_duration=final_trial_duration,
1633             final_relative_width=final_relative_width,
1634             number_of_intermediate_phases=number_of_intermediate_phases,
1635             initial_trial_duration=initial_trial_duration,
1636             timeout=timeout,
1637             debug=logger.debug,
1638             expansion_coefficient=expansion_coefficient,
1639         )
1640         if packet_loss_ratio:
1641             packet_loss_ratios = [0.0, packet_loss_ratio]
1642         else:
1643             # Happens in reconf tests.
1644             packet_loss_ratios = [packet_loss_ratio]
1645         results = algorithm.narrow_down_intervals(
1646             min_rate=minimum_transmit_rate,
1647             max_rate=maximum_transmit_rate,
1648             packet_loss_ratios=packet_loss_ratios,
1649         )
1650         return results
1651
1652     @staticmethod
1653     def perform_soak_search(
1654             frame_size,
1655             traffic_profile,
1656             minimum_transmit_rate,
1657             maximum_transmit_rate,
1658             plr_target=1e-7,
1659             tdpt=0.1,
1660             initial_count=50,
1661             timeout=7200.0,
1662             ppta=1,
1663             resetter=None,
1664             trace_enabled=False,
1665             traffic_directions=2,
1666             transaction_duration=0.0,
1667             transaction_scale=0,
1668             transaction_type=u"packet",
1669             use_latency=False,
1670             ramp_up_rate=None,
1671             ramp_up_duration=None,
1672             state_timeout=240.0,
1673     ):
1674         """Setup initialized TG, perform soak search, return avg and stdev.
1675
1676         :param frame_size: Frame size identifier or value [B].
1677         :param traffic_profile: Module name as a traffic profile identifier.
1678             See GPL/traffic_profiles/trex for implemented modules.
1679         :param minimum_transmit_rate: Minimal load in transactions per second.
1680         :param maximum_transmit_rate: Maximal load in transactions per second.
1681         :param plr_target: Ratio of packets lost to achieve [1].
1682         :param tdpt: Trial duration per trial.
1683             The algorithm linearly increases trial duration with trial number,
1684             this is the increment between succesive trials, in seconds.
1685         :param initial_count: Offset to apply before the first trial.
1686             For example initial_count=50 makes first trial to be 51*tdpt long.
1687             This is needed because initial "search" phase of integrator
1688             takes significant time even without any trial results.
1689         :param timeout: The search will stop after this overall time [s].
1690         :param ppta: Packets per transaction, aggregated over directions.
1691             Needed for udp_pps which does not have a good transaction counter,
1692             so we need to compute expected number of packets.
1693             Default: 1.
1694         :param resetter: Callable to reset DUT state for repeated trials.
1695         :param trace_enabled: True if trace enabled else False.
1696             This is very verbose tracing on numeric computations,
1697             do not use in production.
1698             Default: False
1699         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1700             Default: 2
1701         :param transaction_duration: Total expected time to close transaction.
1702         :param transaction_scale: Number of transactions to perform.
1703             0 (default) means unlimited.
1704         :param transaction_type: An identifier specifying which counters
1705             and formulas to use when computing attempted and failed
1706             transactions. Default: "packet".
1707         :param use_latency: Whether to measure latency during the trial.
1708             Default: False.
1709         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1710         :param ramp_up_duration: Duration of ramp-up trials [s].
1711         :param state_timeout: Time of life of DUT state [s].
1712         :type frame_size: str or int
1713         :type traffic_profile: str
1714         :type minimum_transmit_rate: float
1715         :type maximum_transmit_rate: float
1716         :type plr_target: float
1717         :type initial_count: int
1718         :type timeout: float
1719         :type ppta: int
1720         :type resetter: Optional[Callable[[], None]]
1721         :type trace_enabled: bool
1722         :type traffic_directions: int
1723         :type transaction_duration: float
1724         :type transaction_scale: int
1725         :type transaction_type: str
1726         :type use_latency: bool
1727         :type ramp_up_rate: float
1728         :type ramp_up_duration: float
1729         :type state_timeout: float
1730         :returns: Average and stdev of estimated aggregated rate giving PLR.
1731         :rtype: 2-tuple of float
1732         """
1733         tg_instance = BuiltIn().get_library_instance(
1734             u"resources.libraries.python.TrafficGenerator"
1735         )
1736         # Overrides for fixed transaction amount.
1737         # TODO: Move to robot code? We have a single call site
1738         #       but MLRsearch has two and we want the two to be used similarly.
1739         if transaction_scale:
1740             # TODO: What is a good value for max scale?
1741             # TODO: Scale the timeout with transaction scale.
1742             timeout = 7200.0
1743         tg_instance.set_rate_provider_defaults(
1744             frame_size=frame_size,
1745             traffic_profile=traffic_profile,
1746             negative_loss=False,
1747             sleep_till_duration=True,
1748             ppta=ppta,
1749             resetter=resetter,
1750             traffic_directions=traffic_directions,
1751             transaction_duration=transaction_duration,
1752             transaction_scale=transaction_scale,
1753             transaction_type=transaction_type,
1754             use_latency=use_latency,
1755             ramp_up_rate=ramp_up_rate,
1756             ramp_up_duration=ramp_up_duration,
1757             state_timeout=state_timeout,
1758         )
1759         algorithm = PLRsearch(
1760             measurer=tg_instance,
1761             trial_duration_per_trial=tdpt,
1762             packet_loss_ratio_target=plr_target,
1763             trial_number_offset=initial_count,
1764             timeout=timeout,
1765             trace_enabled=trace_enabled,
1766         )
1767         result = algorithm.search(
1768             min_rate=minimum_transmit_rate,
1769             max_rate=maximum_transmit_rate,
1770         )
1771         return result