5d9a0568f187d563c6a494e6354accb4ff9d0ca8
[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                 # Configure TRex.
427                 ports = ''
428                 for port in tg_node[u"interfaces"].values():
429                     if u'Mellanox' not in port.get(u'model'):
430                         ports += f" {port.get(u'pci_address')}"
431
432                 cmd = f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && " \
433                     f"./dpdk_nic_bind.py -u {ports} || true\""
434                 exec_cmd_no_error(
435                     tg_node, cmd, sudo=True,
436                     message=u"Unbind PCI ports from driver failed!"
437                 )
438
439                 # Start TRex.
440                 cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
441                 trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
442                 trex_cmd.add(u"-i")
443                 trex_cmd.add(u"--prefix $(hostname)")
444                 trex_cmd.add(u"--hdrh")
445                 trex_cmd.add(u"--no-scapy-server")
446                 trex_cmd.add_if(u"--astf", osi_layer == u"L7")
447                 # OptionString does not create double space if extra is empty.
448                 trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
449                 inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
450                 cmd = f"sh -c \"{inner_command}\" > /dev/null"
451                 try:
452                     exec_cmd_no_error(tg_node, cmd, sudo=True)
453                 except RuntimeError:
454                     cmd = u"sh -c \"cat /tmp/trex.log\""
455                     exec_cmd_no_error(
456                         tg_node, cmd, sudo=True,
457                         message=u"Get TRex logs failed!"
458                     )
459                     raise RuntimeError(u"Start TRex failed!")
460
461                 # Test T-Rex API responsiveness.
462                 cmd = f"python3 {Constants.REMOTE_FW_DIR}/GPL/tools/trex/"
463                 if osi_layer in (u"L2", u"L3"):
464                     cmd += u"trex_stl_assert.py"
465                 elif osi_layer == u"L7":
466                     cmd += u"trex_astf_assert.py"
467                 else:
468                     raise ValueError(u"Unknown OSI layer!")
469                 try:
470                     exec_cmd_no_error(
471                         tg_node, cmd, sudo=True,
472                         message=u"T-Rex API is not responding!", retries=20
473                     )
474                 except RuntimeError:
475                     continue
476                 return
477             # After max retries TRex is still not responding to API critical
478             # error occurred.
479             exec_cmd(tg_node, u"cat /tmp/trex.log", sudo=True)
480             raise RuntimeError(u"Start T-Rex failed after multiple retries!")
481
482     @staticmethod
483     def is_trex_running(node):
484         """Check if T-Rex is running using pidof.
485
486         :param node: Traffic generator node.
487         :type node: dict
488         :returns: True if T-Rex is running otherwise False.
489         :rtype: bool
490         """
491         ret, _, _ = exec_cmd(node, u"pgrep t-rex", sudo=True)
492         return bool(int(ret) == 0)
493
494     @staticmethod
495     def teardown_traffic_generator(node):
496         """TG teardown.
497
498         :param node: Traffic generator node.
499         :type node: dict
500         :returns: nothing
501         :raises RuntimeError: If node type is not a TG,
502             or if T-Rex teardown fails.
503         """
504         subtype = check_subtype(node)
505         if subtype == NodeSubTypeTG.TREX:
506             exec_cmd_no_error(
507                 node,
508                 u"sh -c "
509                 u"\"if pgrep t-rex; then sudo pkill t-rex && sleep 3; fi\"",
510                 sudo=False,
511                 message=u"T-Rex kill failed!"
512             )
513
514     def trex_astf_stop_remote_exec(self, node):
515         """Execute T-Rex ASTF script on remote node over ssh to stop running
516         traffic.
517
518         Internal state is updated with measurement results.
519
520         :param node: T-Rex generator node.
521         :type node: dict
522         :raises RuntimeError: If stop traffic script fails.
523         """
524         command_line = OptionString().add(u"python3")
525         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
526         command_line.add(f"'{dirname}/trex_astf_stop.py'")
527         command_line.change_prefix(u"--")
528         for index, value in enumerate(self._xstats):
529             if value is not None:
530                 value = value.replace(u"'", u"\"")
531                 command_line.add_equals(f"xstat{index}", f"'{value}'")
532         stdout, _ = exec_cmd_no_error(
533             node, command_line,
534             message=u"T-Rex ASTF runtime error!"
535         )
536         self._parse_traffic_results(stdout)
537
538     def trex_stl_stop_remote_exec(self, node):
539         """Execute T-Rex STL script on remote node over ssh to stop running
540         traffic.
541
542         Internal state is updated with measurement results.
543
544         :param node: T-Rex generator node.
545         :type node: dict
546         :raises RuntimeError: If stop traffic script fails.
547         """
548         command_line = OptionString().add(u"python3")
549         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
550         command_line.add(f"'{dirname}/trex_stl_stop.py'")
551         command_line.change_prefix(u"--")
552         for index, value in enumerate(self._xstats):
553             if value is not None:
554                 value = value.replace(u"'", u"\"")
555                 command_line.add_equals(f"xstat{index}", f"'{value}'")
556         stdout, _ = exec_cmd_no_error(
557             node, command_line,
558             message=u"T-Rex STL runtime error!"
559         )
560         self._parse_traffic_results(stdout)
561
562     def stop_traffic_on_tg(self):
563         """Stop all traffic on TG.
564
565         :returns: Structure containing the result of the measurement.
566         :rtype: ReceiveRateMeasurement
567         :raises ValueError: If TG traffic profile is not supported.
568         """
569         subtype = check_subtype(self._node)
570         if subtype != NodeSubTypeTG.TREX:
571             raise ValueError(f"Unsupported TG subtype: {subtype!r}")
572         if u"trex-astf" in self.traffic_profile:
573             self.trex_astf_stop_remote_exec(self._node)
574         elif u"trex-stl" in self.traffic_profile:
575             self.trex_stl_stop_remote_exec(self._node)
576         else:
577             raise ValueError(u"Unsupported T-Rex traffic profile!")
578         self._stop_time = time.monotonic()
579
580         return self._get_measurement_result()
581
582     def _compute_duration(self, duration, multiplier):
583         """Compute duration for profile driver.
584
585         The final result is influenced by transaction scale and duration limit.
586         It is assumed a higher level function has already set those to self.
587         The duration argument is the target value from search point of view,
588         before the overrides are applied here.
589
590         Minus one (signalling async traffic start) is kept.
591
592         Completeness flag is also included. Duration limited or async trials
593         are not considered complete for ramp-up purposes.
594
595         :param duration: Time expressed in seconds for how long to send traffic.
596         :param multiplier: Traffic rate in transactions per second.
597         :type duration: float
598         :type multiplier: float
599         :returns: New duration and whether it was a complete ramp-up candidate.
600         :rtype: float, bool
601         """
602         if duration < 0.0:
603             # Keep the async -1.
604             return duration, False
605         computed_duration = duration
606         if self.transaction_scale:
607             computed_duration = self.transaction_scale / multiplier
608             # Log the computed duration,
609             # so we can compare with what telemetry suggests
610             # the real duration was.
611             logger.debug(f"Expected duration {computed_duration}")
612         if not self.duration_limit:
613             return computed_duration, True
614         limited_duration = min(computed_duration, self.duration_limit)
615         return limited_duration, (limited_duration == computed_duration)
616
617     def trex_astf_start_remote_exec(
618             self, duration, multiplier, async_call=False):
619         """Execute T-Rex ASTF script on remote node over ssh to start running
620         traffic.
621
622         In sync mode, measurement results are stored internally.
623         In async mode, initial data including xstats are stored internally.
624
625         This method contains the logic to compute duration as maximum time
626         if transaction_scale is nonzero.
627         The transaction_scale argument defines (limits) how many transactions
628         will be started in total. As that amount of transaction can take
629         considerable time (sometimes due to explicit delays in the profile),
630         the real time a trial needs to finish is computed here. For now,
631         in that case the duration argument is ignored, assuming it comes
632         from ASTF-unaware search algorithm. The overall time a single
633         transaction needs is given in parameter transaction_duration,
634         it includes both explicit delays and implicit time it takes
635         to transfer data (or whatever the transaction does).
636
637         Currently it is observed TRex does not start the ASTF traffic
638         immediately, an ad-hoc constant is added to the computed duration
639         to compensate for that.
640
641         If transaction_scale is zero, duration is not recomputed.
642         It is assumed the subsequent result parsing gets the real duration
643         if the traffic stops sooner for any reason.
644
645         Currently, it is assumed traffic profile defines a single transaction.
646         To avoid heavy logic here, the input rate is expected to be in
647         transactions per second, as that directly translates to TRex multiplier,
648         (assuming the profile does not override the default cps value of one).
649
650         :param duration: Time expressed in seconds for how long to send traffic.
651         :param multiplier: Traffic rate in transactions per second.
652         :param async_call: If enabled then don't wait for all incoming traffic.
653         :type duration: float
654         :type multiplier: int
655         :type async_call: bool
656         :raises RuntimeError: In case of T-Rex driver issue.
657         """
658         self.check_mode(TrexMode.ASTF)
659         p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
660         if not isinstance(duration, (float, int)):
661             duration = float(duration)
662
663         # TODO: Refactor the code so duration is computed only once,
664         # and both the initial and the computed durations are logged.
665         computed_duration, _ = self._compute_duration(duration, multiplier)
666
667         command_line = OptionString().add(u"python3")
668         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
669         command_line.add(f"'{dirname}/trex_astf_profile.py'")
670         command_line.change_prefix(u"--")
671         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
672         command_line.add_with_value(
673             u"profile", f"'{dirname}/{self.traffic_profile}.py'"
674         )
675         command_line.add_with_value(u"duration", f"{computed_duration!r}")
676         command_line.add_with_value(u"frame_size", self.frame_size)
677         command_line.add_with_value(
678             u"n_data_frames", Constants.ASTF_N_DATA_FRAMES
679         )
680         command_line.add_with_value(u"multiplier", multiplier)
681         command_line.add_with_value(u"port_0", p_0)
682         command_line.add_with_value(u"port_1", p_1)
683         command_line.add_with_value(
684             u"traffic_directions", self.traffic_directions
685         )
686         command_line.add_if(u"async_start", async_call)
687         command_line.add_if(u"latency", self.use_latency)
688         command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
689         command_line.add_with_value(
690             u"delay", Constants.PERF_TRIAL_ASTF_DELAY
691         )
692
693         self._start_time = time.monotonic()
694         self._rate = multiplier
695         stdout, _ = exec_cmd_no_error(
696             self._node, command_line, timeout=computed_duration + 10.0,
697             message=u"T-Rex ASTF runtime error!"
698         )
699
700         if async_call:
701             # no result
702             self._target_duration = None
703             self._duration = None
704             self._received = None
705             self._sent = None
706             self._loss = None
707             self._latency = None
708             xstats = [None, None]
709             self._l7_data = dict()
710             self._l7_data[u"client"] = dict()
711             self._l7_data[u"client"][u"active_flows"] = None
712             self._l7_data[u"client"][u"established_flows"] = None
713             self._l7_data[u"client"][u"traffic_duration"] = None
714             self._l7_data[u"server"] = dict()
715             self._l7_data[u"server"][u"active_flows"] = None
716             self._l7_data[u"server"][u"established_flows"] = None
717             self._l7_data[u"server"][u"traffic_duration"] = None
718             if u"udp" in self.traffic_profile:
719                 self._l7_data[u"client"][u"udp"] = dict()
720                 self._l7_data[u"client"][u"udp"][u"connects"] = None
721                 self._l7_data[u"client"][u"udp"][u"closed_flows"] = None
722                 self._l7_data[u"client"][u"udp"][u"err_cwf"] = None
723                 self._l7_data[u"server"][u"udp"] = dict()
724                 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = None
725                 self._l7_data[u"server"][u"udp"][u"closed_flows"] = None
726             elif u"tcp" in self.traffic_profile:
727                 self._l7_data[u"client"][u"tcp"] = dict()
728                 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = None
729                 self._l7_data[u"client"][u"tcp"][u"connects"] = None
730                 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = None
731                 self._l7_data[u"client"][u"tcp"][u"connattempt"] = None
732                 self._l7_data[u"server"][u"tcp"] = dict()
733                 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = None
734                 self._l7_data[u"server"][u"tcp"][u"connects"] = None
735                 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = None
736             else:
737                 logger.warn(u"Unsupported T-Rex ASTF traffic profile!")
738             index = 0
739             for line in stdout.splitlines():
740                 if f"Xstats snapshot {index}: " in line:
741                     xstats[index] = line[19:]
742                     index += 1
743                 if index == 2:
744                     break
745             self._xstats = tuple(xstats)
746         else:
747             self._target_duration = duration
748             self._duration = computed_duration
749             self._parse_traffic_results(stdout)
750
751     def trex_stl_start_remote_exec(self, duration, rate, async_call=False):
752         """Execute T-Rex STL script on remote node over ssh to start running
753         traffic.
754
755         In sync mode, measurement results are stored internally.
756         In async mode, initial data including xstats are stored internally.
757
758         Mode-unaware code (e.g. in search algorithms) works with transactions.
759         To keep the logic simple, multiplier is set to that value.
760         As bidirectional traffic profiles send packets in both directions,
761         they are treated as transactions with two packets (one per direction).
762
763         :param duration: Time expressed in seconds for how long to send traffic.
764         :param rate: Traffic rate in transactions per second.
765         :param async_call: If enabled then don't wait for all incoming traffic.
766         :type duration: float
767         :type rate: str
768         :type async_call: bool
769         :raises RuntimeError: In case of T-Rex driver issue.
770         """
771         self.check_mode(TrexMode.STL)
772         p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
773         if not isinstance(duration, (float, int)):
774             duration = float(duration)
775
776         # TODO: Refactor the code so duration is computed only once,
777         # and both the initial and the computed durations are logged.
778         duration, _ = self._compute_duration(duration=duration, multiplier=rate)
779
780         command_line = OptionString().add(u"python3")
781         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
782         command_line.add(f"'{dirname}/trex_stl_profile.py'")
783         command_line.change_prefix(u"--")
784         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
785         command_line.add_with_value(
786             u"profile", f"'{dirname}/{self.traffic_profile}.py'"
787         )
788         command_line.add_with_value(u"duration", f"{duration!r}")
789         command_line.add_with_value(u"frame_size", self.frame_size)
790         command_line.add_with_value(u"rate", f"{rate!r}")
791         command_line.add_with_value(u"port_0", p_0)
792         command_line.add_with_value(u"port_1", p_1)
793         command_line.add_with_value(
794             u"traffic_directions", self.traffic_directions
795         )
796         command_line.add_if(u"async_start", async_call)
797         command_line.add_if(u"latency", self.use_latency)
798         command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
799         command_line.add_with_value(u"delay", Constants.PERF_TRIAL_STL_DELAY)
800
801         # TODO: This is ugly. Handle parsing better.
802         self._start_time = time.monotonic()
803         self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
804         stdout, _ = exec_cmd_no_error(
805             self._node, command_line, timeout=int(duration) + 60,
806             message=u"T-Rex STL runtime error"
807         )
808
809         if async_call:
810             # no result
811             self._target_duration = None
812             self._duration = None
813             self._received = None
814             self._sent = None
815             self._loss = None
816             self._latency = None
817
818             xstats = [None, None]
819             index = 0
820             for line in stdout.splitlines():
821                 if f"Xstats snapshot {index}: " in line:
822                     xstats[index] = line[19:]
823                     index += 1
824                 if index == 2:
825                     break
826             self._xstats = tuple(xstats)
827         else:
828             self._target_duration = duration
829             self._duration = duration
830             self._parse_traffic_results(stdout)
831
832     def send_traffic_on_tg(
833             self,
834             duration,
835             rate,
836             frame_size,
837             traffic_profile,
838             async_call=False,
839             ppta=1,
840             traffic_directions=2,
841             transaction_duration=0.0,
842             transaction_scale=0,
843             transaction_type=u"packet",
844             duration_limit=0.0,
845             use_latency=False,
846             ramp_up_rate=None,
847             ramp_up_duration=None,
848             state_timeout=240.0,
849             ramp_up_only=False,
850         ):
851         """Send traffic from all configured interfaces on TG.
852
853         In async mode, xstats is stored internally,
854         to enable getting correct result when stopping the traffic.
855         In both modes, stdout is returned,
856         but _parse_traffic_results only works in sync output.
857
858         Note that traffic generator uses DPDK driver which might
859         reorder port numbers based on wiring and PCI numbering.
860         This method handles that, so argument values are invariant,
861         but you can see swapped valued in debug logs.
862
863         When transaction_scale is specified, the duration value is ignored
864         and the needed time is computed. For cases where this results in
865         to too long measurement (e.g. teardown trial with small rate),
866         duration_limit is applied (of non-zero), so the trial is stopped sooner.
867
868         Bidirectional STL profiles are treated as transactions with two packets.
869
870         The return value is None for async.
871
872         :param duration: Duration of test traffic generation in seconds.
873         :param rate: Traffic rate in transactions per second.
874         :param frame_size: Frame size (L2) in Bytes.
875         :param traffic_profile: Module name as a traffic profile identifier.
876             See GPL/traffic_profiles/trex for implemented modules.
877         :param async_call: Async mode.
878         :param ppta: Packets per transaction, aggregated over directions.
879             Needed for udp_pps which does not have a good transaction counter,
880             so we need to compute expected number of packets.
881             Default: 1.
882         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
883             Default: 2
884         :param transaction_duration: Total expected time to close transaction.
885         :param transaction_scale: Number of transactions to perform.
886             0 (default) means unlimited.
887         :param transaction_type: An identifier specifying which counters
888             and formulas to use when computing attempted and failed
889             transactions. Default: "packet".
890         :param duration_limit: Zero or maximum limit for computed (or given)
891             duration.
892         :param use_latency: Whether to measure latency during the trial.
893             Default: False.
894         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
895         :param ramp_up_duration: Duration of ramp-up trials [s].
896         :param state_timeout: Time of life of DUT state [s].
897         :param ramp_up_only: If true, do not perform main trial measurement.
898         :type duration: float
899         :type rate: float
900         :type frame_size: str
901         :type traffic_profile: str
902         :type async_call: bool
903         :type ppta: int
904         :type traffic_directions: int
905         :type transaction_duration: float
906         :type transaction_scale: int
907         :type transaction_type: str
908         :type duration_limit: float
909         :type use_latency: bool
910         :type ramp_up_rate: float
911         :type ramp_up_duration: float
912         :type state_timeout: float
913         :type ramp_up_only: bool
914         :returns: TG results.
915         :rtype: ReceiveRateMeasurement or None
916         :raises ValueError: If TG traffic profile is not supported.
917         """
918         self.set_rate_provider_defaults(
919             frame_size=frame_size,
920             traffic_profile=traffic_profile,
921             ppta=ppta,
922             traffic_directions=traffic_directions,
923             transaction_duration=transaction_duration,
924             transaction_scale=transaction_scale,
925             transaction_type=transaction_type,
926             duration_limit=duration_limit,
927             use_latency=use_latency,
928             ramp_up_rate=ramp_up_rate,
929             ramp_up_duration=ramp_up_duration,
930             state_timeout=state_timeout,
931         )
932         return self._send_traffic_on_tg_with_ramp_up(
933             duration=duration,
934             rate=rate,
935             async_call=async_call,
936             ramp_up_only=ramp_up_only,
937         )
938
939     def _send_traffic_on_tg_internal(
940             self, duration, rate, async_call=False):
941         """Send traffic from all configured interfaces on TG.
942
943         This is an internal function, it assumes set_rate_provider_defaults
944         has been called to remember most values.
945         The reason why need to remember various values is that
946         the traffic can be asynchronous, and parsing needs those values.
947         The reason why this is is a separate function from the one
948         which calls set_rate_provider_defaults is that some search algorithms
949         need to specify their own values, and we do not want the measure call
950         to overwrite them with defaults.
951
952         This function is used both for automated ramp-up trials
953         and for explicitly called trials.
954
955         :param duration: Duration of test traffic generation in seconds.
956         :param rate: Traffic rate in transactions per second.
957         :param async_call: Async mode.
958         :type duration: float
959         :type rate: float
960         :type async_call: bool
961         :returns: TG results.
962         :rtype: ReceiveRateMeasurement or None
963         :raises ValueError: If TG traffic profile is not supported.
964         """
965         subtype = check_subtype(self._node)
966         if subtype == NodeSubTypeTG.TREX:
967             if u"trex-astf" in self.traffic_profile:
968                 self.trex_astf_start_remote_exec(
969                     duration, float(rate), async_call
970                 )
971             elif u"trex-stl" in self.traffic_profile:
972                 unit_rate_str = str(rate) + u"pps"
973                 # TODO: Suport transaction_scale et al?
974                 self.trex_stl_start_remote_exec(
975                     duration, unit_rate_str, async_call
976                 )
977             else:
978                 raise ValueError(u"Unsupported T-Rex traffic profile!")
979
980         return None if async_call else self._get_measurement_result()
981
982     def _send_traffic_on_tg_with_ramp_up(
983             self, duration, rate, async_call=False, ramp_up_only=False):
984         """Send traffic from all interfaces on TG, maybe after ramp-up.
985
986         This is an internal function, it assumes set_rate_provider_defaults
987         has been called to remember most values.
988         The reason why need to remember various values is that
989         the traffic can be asynchronous, and parsing needs those values.
990         The reason why this is a separate function from the one
991         which calls set_rate_provider_defaults is that some search algorithms
992         need to specify their own values, and we do not want the measure call
993         to overwrite them with defaults.
994
995         If ramp-up tracking is detected, a computation is performed,
996         and if state timeout is near, trial at ramp-up rate and duration
997         is inserted before the main trial measurement.
998
999         The ramp_up_only parameter forces a ramp-up without immediate
1000         trial measurement, which is useful in case self remembers
1001         a previous ramp-up trial that belongs to a different test (phase).
1002
1003         Return None if trial is async or ramp-up only.
1004
1005         :param duration: Duration of test traffic generation in seconds.
1006         :param rate: Traffic rate in transactions per second.
1007         :param async_call: Async mode.
1008         :param ramp_up_only: If true, do not perform main trial measurement.
1009         :type duration: float
1010         :type rate: float
1011         :type async_call: bool
1012         :type ramp_up_only: bool
1013         :returns: TG results.
1014         :rtype: ReceiveRateMeasurement or None
1015         :raises ValueError: If TG traffic profile is not supported.
1016         """
1017         complete = False
1018         if self.ramp_up_rate:
1019             # Figure out whether we need to insert a ramp-up trial.
1020             # TODO: Give up on async_call=True?
1021             if ramp_up_only or self.ramp_up_start is None:
1022                 # We never ramped up yet (at least not in this test case).
1023                 ramp_up_needed = True
1024             else:
1025                 # We ramped up before, but maybe it was too long ago.
1026                 # Adding a constant overhead to be safe.
1027                 time_now = time.monotonic() + 1.0
1028                 computed_duration, complete = self._compute_duration(
1029                     duration=duration,
1030                     multiplier=rate,
1031                 )
1032                 # There are two conditions for inserting ramp-up.
1033                 # If early sessions are expiring already,
1034                 # or if late sessions are to expire before measurement is over.
1035                 ramp_up_start_delay = time_now - self.ramp_up_start
1036                 ramp_up_stop_delay = time_now - self.ramp_up_stop
1037                 ramp_up_stop_delay += computed_duration
1038                 bigger_delay = max(ramp_up_start_delay, ramp_up_stop_delay)
1039                 # Final boolean decision.
1040                 ramp_up_needed = (bigger_delay >= self.state_timeout)
1041             if ramp_up_needed:
1042                 logger.debug(
1043                     u"State may time out during next real trial, "
1044                     u"inserting a ramp-up trial."
1045                 )
1046                 self.ramp_up_start = time.monotonic()
1047                 self._send_traffic_on_tg_internal(
1048                     duration=self.ramp_up_duration,
1049                     rate=self.ramp_up_rate,
1050                     async_call=async_call,
1051                 )
1052                 self.ramp_up_stop = time.monotonic()
1053                 logger.debug(u"Ramp-up done.")
1054             else:
1055                 logger.debug(
1056                     u"State will probably not time out during next real trial, "
1057                     u"no ramp-up trial needed just yet."
1058                 )
1059         if ramp_up_only:
1060             return None
1061         trial_start = time.monotonic()
1062         result = self._send_traffic_on_tg_internal(
1063             duration=duration,
1064             rate=rate,
1065             async_call=async_call,
1066         )
1067         trial_end = time.monotonic()
1068         if self.ramp_up_rate:
1069             # Optimization: No loss acts as a good ramp-up, if it was complete.
1070             if complete and result is not None and result.loss_count == 0:
1071                 logger.debug(u"Good trial acts as a ramp-up")
1072                 self.ramp_up_start = trial_start
1073                 self.ramp_up_stop = trial_end
1074             else:
1075                 logger.debug(u"Loss or incomplete, does not act as a ramp-up.")
1076         return result
1077
1078     def no_traffic_loss_occurred(self):
1079         """Fail if loss occurred in traffic run.
1080
1081         :returns: nothing
1082         :raises Exception: If loss occured.
1083         """
1084         if self._loss is None:
1085             raise RuntimeError(u"The traffic generation has not been issued")
1086         if self._loss != u"0":
1087             raise RuntimeError(f"Traffic loss occurred: {self._loss}")
1088
1089     def fail_if_no_traffic_forwarded(self):
1090         """Fail if no traffic forwarded.
1091
1092         TODO: Check number of passed transactions instead.
1093
1094         :returns: nothing
1095         :raises Exception: If no traffic forwarded.
1096         """
1097         if self._received is None:
1098             raise RuntimeError(u"The traffic generation has not been issued")
1099         if self._received == 0:
1100             raise RuntimeError(u"No traffic forwarded")
1101
1102     def partial_traffic_loss_accepted(
1103             self, loss_acceptance, loss_acceptance_type):
1104         """Fail if loss is higher then accepted in traffic run.
1105
1106         :param loss_acceptance: Permitted drop ratio or frames count.
1107         :param loss_acceptance_type: Type of permitted loss.
1108         :type loss_acceptance: float
1109         :type loss_acceptance_type: LossAcceptanceType
1110         :returns: nothing
1111         :raises Exception: If loss is above acceptance criteria.
1112         """
1113         if self._loss is None:
1114             raise Exception(u"The traffic generation has not been issued")
1115
1116         if loss_acceptance_type == u"percentage":
1117             loss = (float(self._loss) / float(self._sent)) * 100
1118         elif loss_acceptance_type == u"frames":
1119             loss = float(self._loss)
1120         else:
1121             raise Exception(u"Loss acceptance type not supported")
1122
1123         if loss > float(loss_acceptance):
1124             raise Exception(
1125                 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
1126             )
1127
1128     def _parse_traffic_results(self, stdout):
1129         """Parse stdout of scripts into fields of self.
1130
1131         Block of code to reuse, by sync start, or stop after async.
1132
1133         :param stdout: Text containing the standard output.
1134         :type stdout: str
1135         """
1136         subtype = check_subtype(self._node)
1137         if subtype == NodeSubTypeTG.TREX:
1138             # Last line from console output
1139             line = stdout.splitlines()[-1]
1140             results = line.split(u";")
1141             if results[-1] in (u" ", u""):
1142                 results.pop(-1)
1143             self._result = dict()
1144             for result in results:
1145                 key, value = result.split(u"=", maxsplit=1)
1146                 self._result[key.strip()] = value
1147             logger.info(f"TrafficGen results:\n{self._result}")
1148             self._received = int(self._result.get(u"total_received"), 0)
1149             self._sent = int(self._result.get(u"total_sent", 0))
1150             self._loss = int(self._result.get(u"frame_loss", 0))
1151             self._approximated_duration = \
1152                 self._result.get(u"approximated_duration", 0.0)
1153             if u"manual" not in str(self._approximated_duration):
1154                 self._approximated_duration = float(self._approximated_duration)
1155             self._latency = list()
1156             self._latency.append(self._result.get(u"latency_stream_0(usec)"))
1157             self._latency.append(self._result.get(u"latency_stream_1(usec)"))
1158             if self._mode == TrexMode.ASTF:
1159                 self._l7_data = dict()
1160                 self._l7_data[u"client"] = dict()
1161                 self._l7_data[u"client"][u"sent"] = \
1162                     int(self._result.get(u"client_sent", 0))
1163                 self._l7_data[u"client"][u"received"] = \
1164                     int(self._result.get(u"client_received", 0))
1165                 self._l7_data[u"client"][u"active_flows"] = \
1166                     int(self._result.get(u"client_active_flows", 0))
1167                 self._l7_data[u"client"][u"established_flows"] = \
1168                     int(self._result.get(u"client_established_flows", 0))
1169                 self._l7_data[u"client"][u"traffic_duration"] = \
1170                     float(self._result.get(u"client_traffic_duration", 0.0))
1171                 self._l7_data[u"client"][u"err_rx_throttled"] = \
1172                     int(self._result.get(u"client_err_rx_throttled", 0))
1173                 self._l7_data[u"client"][u"err_c_nf_throttled"] = \
1174                     int(self._result.get(u"client_err_nf_throttled", 0))
1175                 self._l7_data[u"client"][u"err_flow_overflow"] = \
1176                     int(self._result.get(u"client_err_flow_overflow", 0))
1177                 self._l7_data[u"server"] = dict()
1178                 self._l7_data[u"server"][u"active_flows"] = \
1179                     int(self._result.get(u"server_active_flows", 0))
1180                 self._l7_data[u"server"][u"established_flows"] = \
1181                     int(self._result.get(u"server_established_flows", 0))
1182                 self._l7_data[u"server"][u"traffic_duration"] = \
1183                     float(self._result.get(u"server_traffic_duration", 0.0))
1184                 self._l7_data[u"server"][u"err_rx_throttled"] = \
1185                     int(self._result.get(u"client_err_rx_throttled", 0))
1186                 if u"udp" in self.traffic_profile:
1187                     self._l7_data[u"client"][u"udp"] = dict()
1188                     self._l7_data[u"client"][u"udp"][u"connects"] = \
1189                         int(self._result.get(u"client_udp_connects", 0))
1190                     self._l7_data[u"client"][u"udp"][u"closed_flows"] = \
1191                         int(self._result.get(u"client_udp_closed", 0))
1192                     self._l7_data[u"client"][u"udp"][u"tx_bytes"] = \
1193                         int(self._result.get(u"client_udp_tx_bytes", 0))
1194                     self._l7_data[u"client"][u"udp"][u"rx_bytes"] = \
1195                         int(self._result.get(u"client_udp_rx_bytes", 0))
1196                     self._l7_data[u"client"][u"udp"][u"tx_packets"] = \
1197                         int(self._result.get(u"client_udp_tx_packets", 0))
1198                     self._l7_data[u"client"][u"udp"][u"rx_packets"] = \
1199                         int(self._result.get(u"client_udp_rx_packets", 0))
1200                     self._l7_data[u"client"][u"udp"][u"keep_drops"] = \
1201                         int(self._result.get(u"client_udp_keep_drops", 0))
1202                     self._l7_data[u"client"][u"udp"][u"err_cwf"] = \
1203                         int(self._result.get(u"client_err_cwf", 0))
1204                     self._l7_data[u"server"][u"udp"] = dict()
1205                     self._l7_data[u"server"][u"udp"][u"accepted_flows"] = \
1206                         int(self._result.get(u"server_udp_accepts", 0))
1207                     self._l7_data[u"server"][u"udp"][u"closed_flows"] = \
1208                         int(self._result.get(u"server_udp_closed", 0))
1209                     self._l7_data[u"server"][u"udp"][u"tx_bytes"] = \
1210                         int(self._result.get(u"server_udp_tx_bytes", 0))
1211                     self._l7_data[u"server"][u"udp"][u"rx_bytes"] = \
1212                         int(self._result.get(u"server_udp_rx_bytes", 0))
1213                     self._l7_data[u"server"][u"udp"][u"tx_packets"] = \
1214                         int(self._result.get(u"server_udp_tx_packets", 0))
1215                     self._l7_data[u"server"][u"udp"][u"rx_packets"] = \
1216                         int(self._result.get(u"server_udp_rx_packets", 0))
1217                 elif u"tcp" in self.traffic_profile:
1218                     self._l7_data[u"client"][u"tcp"] = dict()
1219                     self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = \
1220                         int(self._result.get(u"client_tcp_connect_inits", 0))
1221                     self._l7_data[u"client"][u"tcp"][u"connects"] = \
1222                         int(self._result.get(u"client_tcp_connects", 0))
1223                     self._l7_data[u"client"][u"tcp"][u"closed_flows"] = \
1224                         int(self._result.get(u"client_tcp_closed", 0))
1225                     self._l7_data[u"client"][u"tcp"][u"connattempt"] = \
1226                         int(self._result.get(u"client_tcp_connattempt", 0))
1227                     self._l7_data[u"client"][u"tcp"][u"tx_bytes"] = \
1228                         int(self._result.get(u"client_tcp_tx_bytes", 0))
1229                     self._l7_data[u"client"][u"tcp"][u"rx_bytes"] = \
1230                         int(self._result.get(u"client_tcp_rx_bytes", 0))
1231                     self._l7_data[u"server"][u"tcp"] = dict()
1232                     self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = \
1233                         int(self._result.get(u"server_tcp_accepts", 0))
1234                     self._l7_data[u"server"][u"tcp"][u"connects"] = \
1235                         int(self._result.get(u"server_tcp_connects", 0))
1236                     self._l7_data[u"server"][u"tcp"][u"closed_flows"] = \
1237                         int(self._result.get(u"server_tcp_closed", 0))
1238                     self._l7_data[u"server"][u"tcp"][u"tx_bytes"] = \
1239                         int(self._result.get(u"server_tcp_tx_bytes", 0))
1240                     self._l7_data[u"server"][u"tcp"][u"rx_bytes"] = \
1241                         int(self._result.get(u"server_tcp_rx_bytes", 0))
1242
1243     def _get_measurement_result(self):
1244         """Return the result of last measurement as ReceiveRateMeasurement.
1245
1246         Separate function, as measurements can end either by time
1247         or by explicit call, this is the common block at the end.
1248
1249         The target_tr field of ReceiveRateMeasurement is in
1250         transactions per second. Transmit count and loss count units
1251         depend on the transaction type. Usually they are in transactions
1252         per second, or aggregated packets per second.
1253
1254         TODO: Fail on running or already reported measurement.
1255
1256         :returns: Structure containing the result of the measurement.
1257         :rtype: ReceiveRateMeasurement
1258         """
1259         try:
1260             # Client duration seems to include a setup period
1261             # where TRex does not send any packets yet.
1262             # Server duration does not include it.
1263             server_data = self._l7_data[u"server"]
1264             approximated_duration = float(server_data[u"traffic_duration"])
1265         except (KeyError, AttributeError, ValueError, TypeError):
1266             approximated_duration = None
1267         try:
1268             if not approximated_duration:
1269                 approximated_duration = float(self._approximated_duration)
1270         except ValueError:  # "manual"
1271             approximated_duration = None
1272         if not approximated_duration:
1273             if self._duration and self._duration > 0:
1274                 # Known recomputed or target duration.
1275                 approximated_duration = self._duration
1276             else:
1277                 # It was an explicit stop.
1278                 if not self._stop_time:
1279                     raise RuntimeError(u"Unable to determine duration.")
1280                 approximated_duration = self._stop_time - self._start_time
1281         target_duration = self._target_duration
1282         if not target_duration:
1283             target_duration = approximated_duration
1284         transmit_rate = self._rate
1285         unsent = 0
1286         if self.transaction_type == u"packet":
1287             partial_attempt_count = self._sent
1288             packet_rate = transmit_rate * self.ppta
1289             # We have a float. TRex way of rounding it is not obvious.
1290             # The biggest source of mismatch is Inter Stream Gap.
1291             # So the code tolerates 10 usec of missing packets.
1292             expected_attempt_count = (target_duration - 1e-5) * packet_rate
1293             expected_attempt_count = math.ceil(expected_attempt_count)
1294             # TRex can send more.
1295             expected_attempt_count = max(expected_attempt_count, self._sent)
1296             unsent = expected_attempt_count - self._sent
1297             pass_count = self._received
1298             fail_count = expected_attempt_count - pass_count
1299         elif self.transaction_type == u"udp_cps":
1300             if not self.transaction_scale:
1301                 raise RuntimeError(u"Add support for no-limit udp_cps.")
1302             partial_attempt_count = self._l7_data[u"client"][u"sent"]
1303             # We do not care whether TG is slow, it should have attempted all.
1304             expected_attempt_count = self.transaction_scale
1305             unsent = expected_attempt_count - partial_attempt_count
1306             pass_count = self._l7_data[u"client"][u"received"]
1307             fail_count = expected_attempt_count - pass_count
1308         elif self.transaction_type == u"tcp_cps":
1309             if not self.transaction_scale:
1310                 raise RuntimeError(u"Add support for no-limit tcp_cps.")
1311             ctca = self._l7_data[u"client"][u"tcp"][u"connattempt"]
1312             partial_attempt_count = ctca
1313             # We do not care whether TG is slow, it should have attempted all.
1314             expected_attempt_count = self.transaction_scale
1315             unsent = expected_attempt_count - partial_attempt_count
1316             # From TCP point of view, server/connects counts full connections,
1317             # but we are testing NAT session so client/connects counts that
1318             # (half connections from TCP point of view).
1319             pass_count = self._l7_data[u"client"][u"tcp"][u"connects"]
1320             fail_count = expected_attempt_count - pass_count
1321         elif self.transaction_type == u"udp_pps":
1322             if not self.transaction_scale:
1323                 raise RuntimeError(u"Add support for no-limit udp_pps.")
1324             partial_attempt_count = self._sent
1325             expected_attempt_count = self.transaction_scale * self.ppta
1326             unsent = expected_attempt_count - self._sent
1327             fail_count = self._loss + unsent
1328         elif self.transaction_type == u"tcp_pps":
1329             if not self.transaction_scale:
1330                 raise RuntimeError(u"Add support for no-limit tcp_pps.")
1331             partial_attempt_count = self._sent
1332             expected_attempt_count = self.transaction_scale * self.ppta
1333             # One loss-like scenario happens when TRex receives all packets
1334             # on L2 level, but is not fast enough to process them all
1335             # at L7 level, which leads to retransmissions.
1336             # Those manifest as opackets larger than expected.
1337             # A simple workaround is to add absolute difference.
1338             # Probability of retransmissions exactly cancelling
1339             # packets unsent due to duration stretching is quite low.
1340             unsent = abs(expected_attempt_count - self._sent)
1341             fail_count = self._loss + unsent
1342         else:
1343             raise RuntimeError(f"Unknown parsing {self.transaction_type!r}")
1344         if unsent and isinstance(self._approximated_duration, float):
1345             # Do not report unsent for "manual".
1346             logger.debug(f"Unsent packets/transactions: {unsent}")
1347         if fail_count < 0 and not self.negative_loss:
1348             fail_count = 0
1349         measurement = ReceiveRateMeasurement(
1350             duration=target_duration,
1351             target_tr=transmit_rate,
1352             transmit_count=expected_attempt_count,
1353             loss_count=fail_count,
1354             approximated_duration=approximated_duration,
1355             partial_transmit_count=partial_attempt_count,
1356         )
1357         measurement.latency = self.get_latency_int()
1358         return measurement
1359
1360     def measure(self, duration, transmit_rate):
1361         """Run trial measurement, parse and return results.
1362
1363         The input rate is for transactions. Stateles bidirectional traffic
1364         is understood as sequence of (asynchronous) transactions,
1365         two packets each.
1366
1367         The result units depend on test type, generally
1368         the count either transactions or packets (aggregated over directions).
1369
1370         Optionally, this method sleeps if measurement finished before
1371         the time specified as duration.
1372
1373         :param duration: Trial duration [s].
1374         :param transmit_rate: Target rate in transactions per second.
1375         :type duration: float
1376         :type transmit_rate: float
1377         :returns: Structure containing the result of the measurement.
1378         :rtype: ReceiveRateMeasurement
1379         :raises RuntimeError: If TG is not set or if node is not TG
1380             or if subtype is not specified.
1381         :raises NotImplementedError: If TG is not supported.
1382         """
1383         duration = float(duration)
1384         time_start = time.monotonic()
1385         time_stop = time_start + duration
1386         if self.resetter:
1387             self.resetter()
1388         result = self._send_traffic_on_tg_with_ramp_up(
1389             duration=duration,
1390             rate=transmit_rate,
1391             async_call=False,
1392         )
1393         logger.debug(f"trial measurement result: {result!r}")
1394         # In PLRsearch, computation needs the specified time to complete.
1395         if self.sleep_till_duration:
1396             sleeptime = time_stop - time.monotonic()
1397             if sleeptime > 0.0:
1398                 # TODO: Sometimes we have time to do additional trials here,
1399                 # adapt PLRsearch to accept all the results.
1400                 time.sleep(sleeptime)
1401         return result
1402
1403     def set_rate_provider_defaults(
1404             self,
1405             frame_size,
1406             traffic_profile,
1407             ppta=1,
1408             resetter=None,
1409             traffic_directions=2,
1410             transaction_duration=0.0,
1411             transaction_scale=0,
1412             transaction_type=u"packet",
1413             duration_limit=0.0,
1414             negative_loss=True,
1415             sleep_till_duration=False,
1416             use_latency=False,
1417             ramp_up_rate=None,
1418             ramp_up_duration=None,
1419             state_timeout=240.0,
1420         ):
1421         """Store values accessed by measure().
1422
1423         :param frame_size: Frame size identifier or value [B].
1424         :param traffic_profile: Module name as a traffic profile identifier.
1425             See GPL/traffic_profiles/trex for implemented modules.
1426         :param ppta: Packets per transaction, aggregated over directions.
1427             Needed for udp_pps which does not have a good transaction counter,
1428             so we need to compute expected number of packets.
1429             Default: 1.
1430         :param resetter: Callable to reset DUT state for repeated trials.
1431         :param traffic_directions: Traffic from packet counting point of view
1432             is bi- (2) or uni- (1) directional.
1433             Default: 2
1434         :param transaction_duration: Total expected time to close transaction.
1435         :param transaction_scale: Number of transactions to perform.
1436             0 (default) means unlimited.
1437         :param transaction_type: An identifier specifying which counters
1438             and formulas to use when computing attempted and failed
1439             transactions. Default: "packet".
1440             TODO: Does this also specify parsing for the measured duration?
1441         :param duration_limit: Zero or maximum limit for computed (or given)
1442             duration.
1443         :param negative_loss: If false, negative loss is reported as zero loss.
1444         :param sleep_till_duration: If true and measurement returned faster,
1445             sleep until it matches duration. Needed for PLRsearch.
1446         :param use_latency: Whether to measure latency during the trial.
1447             Default: False.
1448         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1449         :param ramp_up_duration: Duration of ramp-up trials [s].
1450         :param state_timeout: Time of life of DUT state [s].
1451         :type frame_size: str or int
1452         :type traffic_profile: str
1453         :type ppta: int
1454         :type resetter: Optional[Callable[[], None]]
1455         :type traffic_directions: int
1456         :type transaction_duration: float
1457         :type transaction_scale: int
1458         :type transaction_type: str
1459         :type duration_limit: float
1460         :type negative_loss: bool
1461         :type sleep_till_duration: bool
1462         :type use_latency: bool
1463         :type ramp_up_rate: float
1464         :type ramp_up_duration: float
1465         :type state_timeout: float
1466         """
1467         self.frame_size = frame_size
1468         self.traffic_profile = str(traffic_profile)
1469         self.resetter = resetter
1470         self.ppta = ppta
1471         self.traffic_directions = int(traffic_directions)
1472         self.transaction_duration = float(transaction_duration)
1473         self.transaction_scale = int(transaction_scale)
1474         self.transaction_type = str(transaction_type)
1475         self.duration_limit = float(duration_limit)
1476         self.negative_loss = bool(negative_loss)
1477         self.sleep_till_duration = bool(sleep_till_duration)
1478         self.use_latency = bool(use_latency)
1479         self.ramp_up_rate = float(ramp_up_rate)
1480         self.ramp_up_duration = float(ramp_up_duration)
1481         self.state_timeout = float(state_timeout)
1482
1483
1484 class OptimizedSearch:
1485     """Class to be imported as Robot Library, containing search keywords.
1486
1487     Aside of setting up measurer and forwarding arguments,
1488     the main business is to translate min/max rate from unidir to aggregated.
1489     """
1490
1491     @staticmethod
1492     def perform_optimized_ndrpdr_search(
1493             frame_size,
1494             traffic_profile,
1495             minimum_transmit_rate,
1496             maximum_transmit_rate,
1497             packet_loss_ratio=0.005,
1498             final_relative_width=0.005,
1499             final_trial_duration=30.0,
1500             initial_trial_duration=1.0,
1501             number_of_intermediate_phases=2,
1502             timeout=1200.0,
1503             ppta=1,
1504             resetter=None,
1505             traffic_directions=2,
1506             transaction_duration=0.0,
1507             transaction_scale=0,
1508             transaction_type=u"packet",
1509             use_latency=False,
1510             ramp_up_rate=None,
1511             ramp_up_duration=None,
1512             state_timeout=240.0,
1513             expansion_coefficient=4.0,
1514     ):
1515         """Setup initialized TG, perform optimized search, return intervals.
1516
1517         If transaction_scale is nonzero, all init and non-init trial durations
1518         are set to 1.0 (as they do not affect the real trial duration)
1519         and zero intermediate phases are used.
1520         This way no re-measurement happens.
1521         Warmup has to be handled via resetter or ramp-up mechanisms.
1522
1523         :param frame_size: Frame size identifier or value [B].
1524         :param traffic_profile: Module name as a traffic profile identifier.
1525             See GPL/traffic_profiles/trex for implemented modules.
1526         :param minimum_transmit_rate: Minimal load in transactions per second.
1527         :param maximum_transmit_rate: Maximal load in transactions per second.
1528         :param packet_loss_ratio: Ratio of packets lost, for PDR [1].
1529         :param final_relative_width: Final lower bound transmit rate
1530             cannot be more distant that this multiple of upper bound [1].
1531         :param final_trial_duration: Trial duration for the final phase [s].
1532         :param initial_trial_duration: Trial duration for the initial phase
1533             and also for the first intermediate phase [s].
1534         :param number_of_intermediate_phases: Number of intermediate phases
1535             to perform before the final phase [1].
1536         :param timeout: The search will fail itself when not finished
1537             before this overall time [s].
1538         :param ppta: Packets per transaction, aggregated over directions.
1539             Needed for udp_pps which does not have a good transaction counter,
1540             so we need to compute expected number of packets.
1541             Default: 1.
1542         :param resetter: Callable to reset DUT state for repeated trials.
1543         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1544             Default: 2
1545         :param transaction_duration: Total expected time to close transaction.
1546         :param transaction_scale: Number of transactions to perform.
1547             0 (default) means unlimited.
1548         :param transaction_type: An identifier specifying which counters
1549             and formulas to use when computing attempted and failed
1550             transactions. Default: "packet".
1551         :param use_latency: Whether to measure latency during the trial.
1552             Default: False.
1553         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1554         :param ramp_up_duration: Duration of ramp-up trials [s].
1555         :param state_timeout: Time of life of DUT state [s].
1556         :param expansion_coefficient: In external search multiply width by this.
1557         :type frame_size: str or int
1558         :type traffic_profile: str
1559         :type minimum_transmit_rate: float
1560         :type maximum_transmit_rate: float
1561         :type packet_loss_ratio: float
1562         :type final_relative_width: float
1563         :type final_trial_duration: float
1564         :type initial_trial_duration: float
1565         :type number_of_intermediate_phases: int
1566         :type timeout: float
1567         :type ppta: int
1568         :type resetter: Optional[Callable[[], None]]
1569         :type traffic_directions: int
1570         :type transaction_duration: float
1571         :type transaction_scale: int
1572         :type transaction_type: str
1573         :type use_latency: bool
1574         :type ramp_up_rate: float
1575         :type ramp_up_duration: float
1576         :type state_timeout: float
1577         :type expansion_coefficient: float
1578         :returns: Structure containing narrowed down NDR and PDR intervals
1579             and their measurements.
1580         :rtype: List[Receiverateinterval]
1581         :raises RuntimeError: If total duration is larger than timeout.
1582         """
1583         # we need instance of TrafficGenerator instantiated by Robot Framework
1584         # to be able to use trex_stl-*()
1585         tg_instance = BuiltIn().get_library_instance(
1586             u"resources.libraries.python.TrafficGenerator"
1587         )
1588         # Overrides for fixed transaction amount.
1589         # TODO: Move to robot code? We have two call sites, so this saves space,
1590         #       even though this is surprising for log readers.
1591         if transaction_scale:
1592             initial_trial_duration = 1.0
1593             final_trial_duration = 1.0
1594             number_of_intermediate_phases = 0
1595             timeout += transaction_scale * 3e-4
1596         tg_instance.set_rate_provider_defaults(
1597             frame_size=frame_size,
1598             traffic_profile=traffic_profile,
1599             sleep_till_duration=False,
1600             ppta=ppta,
1601             resetter=resetter,
1602             traffic_directions=traffic_directions,
1603             transaction_duration=transaction_duration,
1604             transaction_scale=transaction_scale,
1605             transaction_type=transaction_type,
1606             use_latency=use_latency,
1607             ramp_up_rate=ramp_up_rate,
1608             ramp_up_duration=ramp_up_duration,
1609             state_timeout=state_timeout,
1610         )
1611         algorithm = MultipleLossRatioSearch(
1612             measurer=tg_instance,
1613             final_trial_duration=final_trial_duration,
1614             final_relative_width=final_relative_width,
1615             number_of_intermediate_phases=number_of_intermediate_phases,
1616             initial_trial_duration=initial_trial_duration,
1617             timeout=timeout,
1618             debug=logger.debug,
1619             expansion_coefficient=expansion_coefficient,
1620         )
1621         if packet_loss_ratio:
1622             packet_loss_ratios = [0.0, packet_loss_ratio]
1623         else:
1624             # Happens in reconf tests.
1625             packet_loss_ratios = [packet_loss_ratio]
1626         results = algorithm.narrow_down_intervals(
1627             min_rate=minimum_transmit_rate,
1628             max_rate=maximum_transmit_rate,
1629             packet_loss_ratios=packet_loss_ratios,
1630         )
1631         return results
1632
1633     @staticmethod
1634     def perform_soak_search(
1635             frame_size,
1636             traffic_profile,
1637             minimum_transmit_rate,
1638             maximum_transmit_rate,
1639             plr_target=1e-7,
1640             tdpt=0.1,
1641             initial_count=50,
1642             timeout=7200.0,
1643             ppta=1,
1644             resetter=None,
1645             trace_enabled=False,
1646             traffic_directions=2,
1647             transaction_duration=0.0,
1648             transaction_scale=0,
1649             transaction_type=u"packet",
1650             use_latency=False,
1651             ramp_up_rate=None,
1652             ramp_up_duration=None,
1653             state_timeout=240.0,
1654     ):
1655         """Setup initialized TG, perform soak search, return avg and stdev.
1656
1657         :param frame_size: Frame size identifier or value [B].
1658         :param traffic_profile: Module name as a traffic profile identifier.
1659             See GPL/traffic_profiles/trex for implemented modules.
1660         :param minimum_transmit_rate: Minimal load in transactions per second.
1661         :param maximum_transmit_rate: Maximal load in transactions per second.
1662         :param plr_target: Ratio of packets lost to achieve [1].
1663         :param tdpt: Trial duration per trial.
1664             The algorithm linearly increases trial duration with trial number,
1665             this is the increment between succesive trials, in seconds.
1666         :param initial_count: Offset to apply before the first trial.
1667             For example initial_count=50 makes first trial to be 51*tdpt long.
1668             This is needed because initial "search" phase of integrator
1669             takes significant time even without any trial results.
1670         :param timeout: The search will stop after this overall time [s].
1671         :param ppta: Packets per transaction, aggregated over directions.
1672             Needed for udp_pps which does not have a good transaction counter,
1673             so we need to compute expected number of packets.
1674             Default: 1.
1675         :param resetter: Callable to reset DUT state for repeated trials.
1676         :param trace_enabled: True if trace enabled else False.
1677             This is very verbose tracing on numeric computations,
1678             do not use in production.
1679             Default: False
1680         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1681             Default: 2
1682         :param transaction_duration: Total expected time to close transaction.
1683         :param transaction_scale: Number of transactions to perform.
1684             0 (default) means unlimited.
1685         :param transaction_type: An identifier specifying which counters
1686             and formulas to use when computing attempted and failed
1687             transactions. Default: "packet".
1688         :param use_latency: Whether to measure latency during the trial.
1689             Default: False.
1690         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1691         :param ramp_up_duration: Duration of ramp-up trials [s].
1692         :param state_timeout: Time of life of DUT state [s].
1693         :type frame_size: str or int
1694         :type traffic_profile: str
1695         :type minimum_transmit_rate: float
1696         :type maximum_transmit_rate: float
1697         :type plr_target: float
1698         :type initial_count: int
1699         :type timeout: float
1700         :type ppta: int
1701         :type resetter: Optional[Callable[[], None]]
1702         :type trace_enabled: bool
1703         :type traffic_directions: int
1704         :type transaction_duration: float
1705         :type transaction_scale: int
1706         :type transaction_type: str
1707         :type use_latency: bool
1708         :type ramp_up_rate: float
1709         :type ramp_up_duration: float
1710         :type state_timeout: float
1711         :returns: Average and stdev of estimated aggregated rate giving PLR.
1712         :rtype: 2-tuple of float
1713         """
1714         tg_instance = BuiltIn().get_library_instance(
1715             u"resources.libraries.python.TrafficGenerator"
1716         )
1717         # Overrides for fixed transaction amount.
1718         # TODO: Move to robot code? We have a single call site
1719         #       but MLRsearch has two and we want the two to be used similarly.
1720         if transaction_scale:
1721             # TODO: What is a good value for max scale?
1722             # TODO: Scale the timeout with transaction scale.
1723             timeout = 7200.0
1724         tg_instance.set_rate_provider_defaults(
1725             frame_size=frame_size,
1726             traffic_profile=traffic_profile,
1727             negative_loss=False,
1728             sleep_till_duration=True,
1729             ppta=ppta,
1730             resetter=resetter,
1731             traffic_directions=traffic_directions,
1732             transaction_duration=transaction_duration,
1733             transaction_scale=transaction_scale,
1734             transaction_type=transaction_type,
1735             use_latency=use_latency,
1736             ramp_up_rate=ramp_up_rate,
1737             ramp_up_duration=ramp_up_duration,
1738             state_timeout=state_timeout,
1739         )
1740         algorithm = PLRsearch(
1741             measurer=tg_instance,
1742             trial_duration_per_trial=tdpt,
1743             packet_loss_ratio_target=plr_target,
1744             trial_number_offset=initial_count,
1745             timeout=timeout,
1746             trace_enabled=trace_enabled,
1747         )
1748         result = algorithm.search(
1749             min_rate=minimum_transmit_rate,
1750             max_rate=maximum_transmit_rate,
1751         )
1752         return result