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