T-Rex: CPU pinning
[csit.git] / resources / libraries / python / TrafficGenerator.py
1 # Copyright (c) 2020 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 time
17
18 from robot.api import logger
19 from robot.libraries.BuiltIn import BuiltIn
20
21 from .Constants import Constants
22 from .CpuUtils import CpuUtils
23 from .DropRateSearch import DropRateSearch
24 from .MLRsearch.AbstractMeasurer import AbstractMeasurer
25 from .MLRsearch.MultipleLossRatioSearch import MultipleLossRatioSearch
26 from .MLRsearch.ReceiveRateMeasurement import ReceiveRateMeasurement
27 from .PLRsearch.PLRsearch import PLRsearch
28 from .OptionString import OptionString
29 from .ssh import exec_cmd_no_error, exec_cmd
30 from .topology import NodeType
31 from .topology import NodeSubTypeTG
32 from .topology import Topology
33
34 __all__ = [u"TGDropRateSearchImpl", u"TrafficGenerator", u"OptimizedSearch"]
35
36
37 def check_subtype(node):
38     """Return supported subtype of given node, or raise an exception.
39
40     Currently only one subtype is supported,
41     but we want our code to be ready for other ones.
42
43     :param node: Topology node to check. Can be None.
44     :type node: dict or NoneType
45     :returns: Subtype detected.
46     :rtype: NodeSubTypeTG
47     :raises RuntimeError: If node is not supported, message explains how.
48     """
49     if node.get(u"type") is None:
50         msg = u"Node type is not defined"
51     elif node['type'] != NodeType.TG:
52         msg = f"Node type is {node[u'type']!r}, not a TG"
53     elif node.get(u"subtype") is None:
54         msg = u"TG subtype is not defined"
55     elif node[u"subtype"] != NodeSubTypeTG.TREX:
56         msg = f"TG subtype {node[u'subtype']!r} is not supported"
57     else:
58         return NodeSubTypeTG.TREX
59     raise RuntimeError(msg)
60
61
62 class TGDropRateSearchImpl(DropRateSearch):
63     """Drop Rate Search implementation."""
64
65     # def __init__(self):
66     #     super(TGDropRateSearchImpl, self).__init__()
67
68     def measure_loss(
69             self, rate, frame_size, loss_acceptance, loss_acceptance_type,
70             traffic_profile, skip_warmup=False):
71         """Runs the traffic and evaluate the measured results.
72
73         :param rate: Offered traffic load.
74         :param frame_size: Size of frame.
75         :param loss_acceptance: Permitted drop ratio or frames count.
76         :param loss_acceptance_type: Type of permitted loss.
77         :param traffic_profile: Module name as a traffic profile identifier.
78             See resources/traffic_profiles/trex for implemented modules.
79         :param skip_warmup: Start TRex without warmup traffic if true.
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         :type skip_warmup: bool
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             if skip_warmup:
100                 tg_instance.trex_stl_start_remote_exec(
101                     self.get_duration(), unit_rate, frame_size, traffic_profile,
102                     warmup_time=0.0
103                 )
104             else:
105                 tg_instance.trex_stl_start_remote_exec(
106                     self.get_duration(), unit_rate, frame_size, traffic_profile
107                 )
108             loss = tg_instance.get_loss()
109             sent = tg_instance.get_sent()
110             if self.loss_acceptance_type_is_percentage():
111                 loss = (float(loss) / float(sent)) * 100
112             logger.trace(
113                 f"comparing: {loss} < {loss_acceptance} {loss_acceptance_type}"
114             )
115             return float(loss) <= float(loss_acceptance)
116         return False
117
118     def get_latency(self):
119         """Returns min/avg/max latency.
120
121         :returns: Latency stats.
122         :rtype: list
123         """
124         tg_instance = BuiltIn().get_library_instance(
125             u"resources.libraries.python.TrafficGenerator"
126         )
127         return tg_instance.get_latency_int()
128
129
130 # TODO: Pylint says too-many-instance-attributes.
131 # A fix is developed in https://gerrit.fd.io/r/c/csit/+/22221
132 class TrafficGenerator(AbstractMeasurer):
133     """Traffic Generator.
134
135     FIXME: Describe API."""
136
137     # TODO: Decrease friction between various search and rate provider APIs.
138     # TODO: Remove "trex" from lines which could work with other TGs.
139
140     # Use one instance of TrafficGenerator for all tests in test suite
141     ROBOT_LIBRARY_SCOPE = u"TEST SUITE"
142
143     def __init__(self):
144         # TODO: Number of fields will be reduced with CSIT-1378.
145         self._node = None
146         # T-REX 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         # Measurement input fields, needed for async stop result.
155         self._start_time = None
156         self._rate = None
157         # Other input parameters, not knowable from measure() signature.
158         self.frame_size = None
159         self.traffic_profile = None
160         self.warmup_time = None
161         self.traffic_directions = None
162         # Transient data needed for async measurements.
163         self._xstats = (None, None)
164         # TODO: Rename "xstats" to something opaque, so TRex is not privileged?
165
166     @property
167     def node(self):
168         """Getter.
169
170         :returns: Traffic generator node.
171         :rtype: dict
172         """
173         return self._node
174
175     def get_loss(self):
176         """Return number of lost packets.
177
178         :returns: Number of lost packets.
179         :rtype: str
180         """
181         return self._loss
182
183     def get_sent(self):
184         """Return number of sent packets.
185
186         :returns: Number of sent packets.
187         :rtype: str
188         """
189         return self._sent
190
191     def get_received(self):
192         """Return number of received packets.
193
194         :returns: Number of received packets.
195         :rtype: str
196         """
197         return self._received
198
199     def get_latency_int(self):
200         """Return rounded min/avg/max latency.
201
202         :returns: Latency stats.
203         :rtype: list
204         """
205         return self._latency
206
207     # TODO: pylint says disable=too-many-locals.
208     # A fix is developed in https://gerrit.fd.io/r/c/csit/+/22221
209     def initialize_traffic_generator(
210             self, tg_node, tg_if1, tg_if2, tg_if1_adj_node, tg_if1_adj_if,
211             tg_if2_adj_node, tg_if2_adj_if, osi_layer, tg_if1_dst_mac=None,
212             tg_if2_dst_mac=None):
213         """TG initialization.
214
215         TODO: Document why do we need (and how do we use) _ifaces_reordered.
216
217         :param tg_node: Traffic generator node.
218         :param tg_if1: TG - name of first interface.
219         :param tg_if2: TG - name of second interface.
220         :param tg_if1_adj_node: TG if1 adjecent node.
221         :param tg_if1_adj_if: TG if1 adjecent interface.
222         :param tg_if2_adj_node: TG if2 adjecent node.
223         :param tg_if2_adj_if: TG if2 adjecent interface.
224         :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
225         :param tg_if1_dst_mac: Interface 1 destination MAC address.
226         :param tg_if2_dst_mac: Interface 2 destination MAC address.
227         :type tg_node: dict
228         :type tg_if1: str
229         :type tg_if2: str
230         :type tg_if1_adj_node: dict
231         :type tg_if1_adj_if: str
232         :type tg_if2_adj_node: dict
233         :type tg_if2_adj_if: str
234         :type osi_layer: str
235         :type tg_if1_dst_mac: str
236         :type tg_if2_dst_mac: str
237         :returns: nothing
238         :raises RuntimeError: In case of issue during initialization.
239         """
240         subtype = check_subtype(tg_node)
241         if subtype == NodeSubTypeTG.TREX:
242             self._node = tg_node
243
244             if1_pci = Topology().get_interface_pci_addr(self._node, tg_if1)
245             if2_pci = Topology().get_interface_pci_addr(self._node, tg_if2)
246             if1_addr = Topology().get_interface_mac(self._node, tg_if1)
247             if2_addr = Topology().get_interface_mac(self._node, tg_if2)
248
249             if osi_layer == u"L2":
250                 if1_adj_addr = if2_addr
251                 if2_adj_addr = if1_addr
252             elif osi_layer == u"L3":
253                 if1_adj_addr = Topology().get_interface_mac(
254                     tg_if1_adj_node, tg_if1_adj_if
255                 )
256                 if2_adj_addr = Topology().get_interface_mac(
257                     tg_if2_adj_node, tg_if2_adj_if
258                 )
259             elif osi_layer == u"L7":
260                 if1_addr = Topology().get_interface_ip4(self._node, tg_if1)
261                 if2_addr = Topology().get_interface_ip4(self._node, tg_if2)
262                 if1_adj_addr = Topology().get_interface_ip4(
263                     tg_if1_adj_node, tg_if1_adj_if
264                 )
265                 if2_adj_addr = Topology().get_interface_ip4(
266                     tg_if2_adj_node, tg_if2_adj_if
267                 )
268             else:
269                 raise ValueError(u"Unknown Test Type")
270
271             # in case of switched environment we can override MAC addresses
272             if tg_if1_dst_mac is not None and tg_if2_dst_mac is not None:
273                 if1_adj_addr = tg_if1_dst_mac
274                 if2_adj_addr = tg_if2_dst_mac
275
276             if min(if1_pci, if2_pci) != if1_pci:
277                 if1_pci, if2_pci = if2_pci, if1_pci
278                 if1_addr, if2_addr = if2_addr, if1_addr
279                 if1_adj_addr, if2_adj_addr = if2_adj_addr, if1_adj_addr
280                 self._ifaces_reordered = True
281
282             master_thread_id, latency_thread_id, socket, threads = \
283                 CpuUtils.get_affinity_trex(
284                     self._node, tg_if1, tg_if2,
285                     tg_dtc=Constants.TREX_CORE_COUNT)
286
287             if osi_layer in (u"L2", u"L3"):
288                 dst_mac0 = f"0x{if1_adj_addr.replace(u':', u',0x')}"
289                 src_mac0 = f"0x{if1_addr.replace(u':', u',0x')}"
290                 dst_mac1 = f"0x{if2_adj_addr.replace(u':', u',0x')}"
291                 src_mac1 = f"0x{if2_addr.replace(u':', u',0x')}"
292                 exec_cmd_no_error(
293                     self._node,
294                     f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
295                     f"- version: 2\n"
296                     f"  c: {len(threads)}\n"
297                     f"  limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
298                     f"  interfaces: [\"{if1_pci}\",\"{if2_pci}\"]\n"
299                     f"  port_info:\n"
300                     f"      - dest_mac: [{dst_mac0}]\n"
301                     f"        src_mac: [{src_mac0}]\n"
302                     f"      - dest_mac: [{dst_mac1}]\n"
303                     f"        src_mac: [{src_mac1}]\n"
304                     f"  platform :\n"
305                     f"      master_thread_id: {master_thread_id}\n"
306                     f"      latency_thread_id: {latency_thread_id}\n"
307                     f"      dual_if:\n"
308                     f"          - socket: {socket}\n"
309                     f"            threads: {threads}\n"
310                     f"EOF'",
311                     sudo=True, message=u"TRex config generation!"
312                 )
313             elif osi_layer == u"L7":
314                 exec_cmd_no_error(
315                     self._node,
316                     f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
317                     f"- version: 2\n"
318                     f"  c: {len(threads)}\n"
319                     f"  limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
320                     f"  interfaces: [\"{if1_pci}\",\"{if2_pci}\"]\n"
321                     f"  port_info:\n"
322                     f"      - ip: [{if1_addr}]\n"
323                     f"        default_gw: [{if1_adj_addr}]\n"
324                     f"      - ip: [{if2_addr}]\n"
325                     f"        default_gw: [{if2_adj_addr}]\n"
326                     f"  platform :\n"
327                     f"      master_thread_id: {master_thread_id}\n"
328                     f"      latency_thread_id: {latency_thread_id}\n"
329                     f"      dual_if:\n"
330                     f"          - socket: {socket}\n"
331                     f"            threads: {threads}\n"
332                     f"EOF'",
333                     sudo=True, message=u"TRex config generation!"
334                 )
335             else:
336                 raise ValueError(u"Unknown Test Type!")
337
338             self._startup_trex(osi_layer)
339
340     def _startup_trex(self, osi_layer):
341         """Startup sequence for the TRex traffic generator.
342
343         :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
344         :type osi_layer: str
345         :raises RuntimeError: If node subtype is not a TREX or startup failed.
346         """
347         # No need to check subtype, we know it is TREX.
348         for _ in range(0, 3):
349             # Kill TRex only if it is already running.
350             cmd = u"sh -c \"pgrep t-rex && pkill t-rex && sleep 3 || true\""
351             exec_cmd_no_error(
352                 self._node, cmd, sudo=True, message=u"Kill TRex failed!"
353             )
354
355             # Configure TRex.
356             ports = ''
357             for port in self._node[u"interfaces"].values():
358                 ports += f" {port.get(u'pci_address')}"
359
360             cmd = f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && " \
361                 f"./dpdk_nic_bind.py -u {ports} || true\""
362             exec_cmd_no_error(
363                 self._node, cmd, sudo=True,
364                 message=u"Unbind PCI ports from driver failed!"
365             )
366
367             # Start TRex.
368             cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
369             trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
370             trex_cmd.add(u"-i")
371             trex_cmd.add(u"--prefix $(hostname)")
372             trex_cmd.add(u"--hdrh")
373             trex_cmd.add(u"--no-scapy-server")
374             trex_cmd.add_if(u"--astf", osi_layer == u"L7")
375             # OptionString does not create double space if extra is empty.
376             trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
377             inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
378             cmd = f"sh -c \"{inner_command}\" > /dev/null"
379             try:
380                 exec_cmd_no_error(self._node, cmd, sudo=True)
381             except RuntimeError:
382                 cmd = u"sh -c \"cat /tmp/trex.log\""
383                 exec_cmd_no_error(
384                     self._node, cmd, sudo=True, message=u"Get TRex logs failed!"
385                 )
386                 raise RuntimeError(u"Start TRex failed!")
387
388             # Test if TRex starts successfuly.
389             cmd = f"sh -c \"{Constants.REMOTE_FW_DIR}/resources/tools/trex/" \
390                 f"trex_server_info.py\""
391             try:
392                 exec_cmd_no_error(
393                     self._node, cmd, sudo=True, message=u"Test TRex failed!",
394                     retries=20
395                 )
396             except RuntimeError:
397                 continue
398             return
399         # After max retries TRex is still not responding to API critical error
400         # occurred.
401         exec_cmd(self._node, u"cat /tmp/trex.log", sudo=True)
402         raise RuntimeError(u"Start TRex failed after multiple retries!")
403
404     @staticmethod
405     def is_trex_running(node):
406         """Check if TRex is running using pidof.
407
408         :param node: Traffic generator node.
409         :type node: dict
410         :returns: True if TRex is running otherwise False.
411         :rtype: bool
412         :raises RuntimeError: If node type is not a TG.
413         """
414         ret, _, _ = exec_cmd(node, u"pidof t-rex", sudo=True)
415         return bool(int(ret) == 0)
416
417     @staticmethod
418     def teardown_traffic_generator(node):
419         """TG teardown.
420
421         :param node: Traffic generator node.
422         :type node: dict
423         :returns: nothing
424         :raises RuntimeError: If node type is not a TG,
425             or if TRex teardown fails.
426         """
427         subtype = check_subtype(node)
428         if subtype == NodeSubTypeTG.TREX:
429             exec_cmd_no_error(
430                 node, u"sh -c \"sudo pkill t-rex && sleep 3\"",
431                 sudo=False, message=u"pkill t-rex failed"
432             )
433
434     def _parse_traffic_results(self, stdout):
435         """Parse stdout of scripts into fields of self.
436
437         Block of code to reuse, by sync start, or stop after async.
438         TODO: Is the output TG subtype dependent?
439
440         :param stdout: Text containing the standard output.
441         :type stdout: str
442         """
443         # last line from console output
444         line = stdout.splitlines()[-1]
445         self._result = line
446         logger.info(f"TrafficGen result: {self._result}")
447         self._received = self._result.split(u", ")[1].split(u"=", 1)[1]
448         self._sent = self._result.split(u", ")[2].split(u"=", 1)[1]
449         self._loss = self._result.split(u", ")[3].split(u"=", 1)[1]
450         self._latency = list()
451         self._latency.append(self._result.split(u", ")[4].split(u"=", 1)[1])
452         self._latency.append(self._result.split(u", ")[5].split(u"=", 1)[1])
453
454     def trex_stl_stop_remote_exec(self, node):
455         """Execute script on remote node over ssh to stop running traffic.
456
457         Internal state is updated with measurement results.
458
459         :param node: TRex generator node.
460         :type node: dict
461         :raises RuntimeError: If stop traffic script fails.
462         """
463         # No need to check subtype, we know it is TREX.
464         x_args = u""
465         for index, value in enumerate(self._xstats):
466             if value is not None:
467                 # Nested quoting is fun.
468                 value = value.replace(u"'", u"\"")
469                 x_args += f" --xstat{index}='\"'\"'{value}'\"'\"'"
470         stdout, _ = exec_cmd_no_error(
471             node, f"sh -c '{Constants.REMOTE_FW_DIR}/resources/tools/trex/"
472             f"trex_stateless_stop.py{x_args}'",
473             message=u"TRex stateless runtime error"
474         )
475         self._parse_traffic_results(stdout)
476
477     def trex_stl_start_remote_exec(
478             self, duration, rate, frame_size, traffic_profile, async_call=False,
479             latency=True, warmup_time=5.0, traffic_directions=2, tx_port=0,
480             rx_port=1):
481         """Execute script on remote node over ssh to start traffic.
482
483         In sync mode, measurement results are stored internally.
484         In async mode, initial data including xstats are stored internally.
485
486         :param duration: Time expresed in seconds for how long to send traffic.
487         :param rate: Traffic rate expressed with units (pps, %)
488         :param frame_size: L2 frame size to send (without padding and IPG).
489         :param traffic_profile: Module name as a traffic profile identifier.
490             See resources/traffic_profiles/trex for implemented modules.
491         :param async_call: If enabled then don't wait for all incomming trafic.
492         :param latency: With latency measurement.
493         :param warmup_time: Warmup time period.
494         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
495             Default: 2
496         :param tx_port: Traffic generator transmit port for first flow.
497             Default: 0
498         :param rx_port: Traffic generator receive port for first flow.
499             Default: 1
500         :type duration: float
501         :type rate: str
502         :type frame_size: str
503         :type traffic_profile: str
504         :type async_call: bool
505         :type latency: bool
506         :type warmup_time: float
507         :type traffic_directions: int
508         :type tx_port: int
509         :type rx_port: int
510         :raises RuntimeError: In case of TG driver issue.
511         """
512         # No need to check subtype, we know it is TREX.
513         reorder = self._ifaces_reordered  # Just to make the next line fit.
514         p_0, p_1 = (rx_port, tx_port) if reorder else (tx_port, rx_port)
515
516         if not isinstance(duration, (float, int)):
517             duration = float(duration)
518         if not isinstance(warmup_time, (float, int)):
519             warmup_time = float(warmup_time)
520         command = f"sh -c \"" \
521             f"{Constants.REMOTE_FW_DIR}/resources/tools/trex/" \
522             f"trex_stateless_profile.py" \
523             f" --profile {Constants.REMOTE_FW_DIR}/resources/" \
524             f"traffic_profiles/trex/{traffic_profile}.py" \
525             f" --duration {duration!r} --frame_size {frame_size} " \
526             f"--rate {rate!r} --warmup_time {warmup_time!r} " \
527             f"--port_0 {p_0} --port_1 {p_1}" \
528             f" --traffic_directions {traffic_directions}"
529         if async_call:
530             command += u" --async_start"
531         if latency:
532             command += u" --latency"
533         command += u"\""
534
535         stdout, _ = exec_cmd_no_error(
536             self._node, command, timeout=float(duration) + 60,
537             message=u"TRex stateless runtime error"
538         )
539
540         self.traffic_directions = traffic_directions
541         if async_call:
542             # no result
543             self._start_time = time.time()
544             self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
545             self._received = None
546             self._sent = None
547             self._loss = None
548             self._latency = None
549             xstats = [None, None]
550             index = 0
551             for line in stdout.splitlines():
552                 if f"Xstats snapshot {index}: " in line:
553                     xstats[index] = line[19:]
554                     index += 1
555                 if index == 2:
556                     break
557             self._xstats = tuple(xstats)
558         else:
559             self._parse_traffic_results(stdout)
560             self._start_time = None
561             self._rate = None
562
563     def stop_traffic_on_tg(self):
564         """Stop all traffic on TG.
565
566         :returns: Structure containing the result of the measurement.
567         :rtype: ReceiveRateMeasurement
568         :raises RuntimeError: If TG is not set.
569         """
570         subtype = check_subtype(self._node)
571         if subtype == NodeSubTypeTG.TREX:
572             self.trex_stl_stop_remote_exec(self._node)
573         return self.get_measurement_result()
574
575     def send_traffic_on_tg(
576             self, duration, rate, frame_size, traffic_profile, warmup_time=5,
577             async_call=False, latency=True, traffic_directions=2, tx_port=0,
578             rx_port=1):
579         """Send traffic from all configured interfaces on TG.
580
581         In async mode, xstats is stored internally,
582         to enable getting correct result when stopping the traffic.
583         In both modes, stdout is returned,
584         but _parse_traffic_results only works in sync output.
585
586         Note that bidirectional traffic also contains flows
587         transmitted from rx_port and received in tx_port.
588         But some tests use asymmetric traffic, so those arguments are relevant.
589
590         Also note that traffic generator uses DPDK driver which might
591         reorder port numbers based on wiring and PCI numbering.
592         This method handles that, so argument values are invariant,
593         but you can see swapped valued in debug logs.
594
595         TODO: Is it better to have less descriptive argument names
596         just to make them less probable to be viewed as misleading or confusing?
597         See https://gerrit.fd.io/r/#/c/17625/11/resources/libraries/python\
598         /TrafficGenerator.py@406
599
600         :param duration: Duration of test traffic generation in seconds.
601         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
602         :param frame_size: Frame size (L2) in Bytes.
603         :param traffic_profile: Module name as a traffic profile identifier.
604             See resources/traffic_profiles/trex for implemented modules.
605         :param warmup_time: Warmup phase in seconds.
606         :param async_call: Async mode.
607         :param latency: With latency measurement.
608         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
609             Default: 2
610         :param tx_port: Traffic generator transmit port for first flow.
611             Default: 0
612         :param rx_port: Traffic generator receive port for first flow.
613             Default: 1
614         :type duration: str
615         :type rate: str
616         :type frame_size: str
617         :type traffic_profile: str
618         :type warmup_time: float
619         :type async_call: bool
620         :type latency: bool
621         :type traffic_directions: int
622         :type tx_port: int
623         :type rx_port: int
624         :returns: TG output.
625         :rtype: str
626         :raises RuntimeError: If TG is not set, or if node is not TG,
627             or if subtype is not specified.
628         :raises NotImplementedError: If TG is not supported.
629         """
630         subtype = check_subtype(self._node)
631         if subtype == NodeSubTypeTG.TREX:
632             self.trex_stl_start_remote_exec(
633                 duration, rate, frame_size, traffic_profile, async_call,
634                 latency, warmup_time, traffic_directions, tx_port, rx_port
635             )
636
637         return self._result
638
639     def no_traffic_loss_occurred(self):
640         """Fail if loss occurred in traffic run.
641
642         :returns: nothing
643         :raises Exception: If loss occured.
644         """
645         if self._loss is None:
646             raise RuntimeError(u"The traffic generation has not been issued")
647         if self._loss != u"0":
648             raise RuntimeError(f"Traffic loss occurred: {self._loss}")
649
650     def fail_if_no_traffic_forwarded(self):
651         """Fail if no traffic forwarded.
652
653         :returns: nothing
654         :raises Exception: If no traffic forwarded.
655         """
656         if self._received is None:
657             raise RuntimeError(u"The traffic generation has not been issued")
658         if self._received == u"0":
659             raise RuntimeError(u"No traffic forwarded")
660
661     def partial_traffic_loss_accepted(
662             self, loss_acceptance, loss_acceptance_type):
663         """Fail if loss is higher then accepted in traffic run.
664
665         :param loss_acceptance: Permitted drop ratio or frames count.
666         :param loss_acceptance_type: Type of permitted loss.
667         :type loss_acceptance: float
668         :type loss_acceptance_type: LossAcceptanceType
669         :returns: nothing
670         :raises Exception: If loss is above acceptance criteria.
671         """
672         if self._loss is None:
673             raise Exception(u"The traffic generation has not been issued")
674
675         if loss_acceptance_type == u"percentage":
676             loss = (float(self._loss) / float(self._sent)) * 100
677         elif loss_acceptance_type == u"frames":
678             loss = float(self._loss)
679         else:
680             raise Exception(u"Loss acceptance type not supported")
681
682         if loss > float(loss_acceptance):
683             raise Exception(
684                 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
685             )
686
687     def set_rate_provider_defaults(
688             self, frame_size, traffic_profile, warmup_time=0.0,
689             traffic_directions=2):
690         """Store values accessed by measure().
691
692         :param frame_size: Frame size identifier or value [B].
693         :param traffic_profile: Module name as a traffic profile identifier.
694             See resources/traffic_profiles/trex for implemented modules.
695         :param warmup_time: Traffic duration before measurement starts [s].
696         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
697             Default: 2
698         :type frame_size: str or int
699         :type traffic_profile: str
700         :type warmup_time: float
701         :type traffic_directions: int
702         """
703         self.frame_size = frame_size
704         self.traffic_profile = str(traffic_profile)
705         self.warmup_time = float(warmup_time)
706         self.traffic_directions = traffic_directions
707
708     def get_measurement_result(self, duration=None, transmit_rate=None):
709         """Return the result of last measurement as ReceiveRateMeasurement.
710
711         Separate function, as measurements can end either by time
712         or by explicit call, this is the common block at the end.
713
714         TODO: Fail on running or already reported measurement.
715
716         :param duration: Measurement duration [s] if known beforehand.
717             For explicitly stopped measurement it is estimated.
718         :param transmit_rate: Target aggregate transmit rate [pps].
719             If not given, computed assuming it was bidirectional.
720         :type duration: float or NoneType
721         :type transmit_rate: float or NoneType
722         :returns: Structure containing the result of the measurement.
723         :rtype: ReceiveRateMeasurement
724         """
725         if duration is None:
726             duration = time.time() - self._start_time
727             self._start_time = None
728         if transmit_rate is None:
729             transmit_rate = self._rate * self.traffic_directions
730         transmit_count = int(self.get_sent())
731         loss_count = int(self.get_loss())
732         measurement = ReceiveRateMeasurement(
733             duration, transmit_rate, transmit_count, loss_count
734         )
735         measurement.latency = self.get_latency_int()
736         return measurement
737
738     def measure(self, duration, transmit_rate):
739         """Run trial measurement, parse and return aggregate results.
740
741         Aggregate means sum over traffic directions.
742
743         :param duration: Trial duration [s].
744         :param transmit_rate: Target aggregate transmit rate [pps].
745         :type duration: float
746         :type transmit_rate: float
747         :returns: Structure containing the result of the measurement.
748         :rtype: ReceiveRateMeasurement
749         :raises RuntimeError: If TG is not set, or if node is not TG,
750             or if subtype is not specified.
751         :raises NotImplementedError: If TG is not supported.
752         """
753         duration = float(duration)
754         transmit_rate = float(transmit_rate)
755         # TG needs target Tr per stream, but reports aggregate Tx and Dx.
756         unit_rate_int = transmit_rate / float(self.traffic_directions)
757         unit_rate_str = str(unit_rate_int) + u"pps"
758         self.send_traffic_on_tg(
759             duration, unit_rate_str, self.frame_size, self.traffic_profile,
760             warmup_time=self.warmup_time, latency=True,
761             traffic_directions=self.traffic_directions
762         )
763         return self.get_measurement_result(duration, transmit_rate)
764
765
766 class OptimizedSearch:
767     """Class to be imported as Robot Library, containing search keywords.
768
769     Aside of setting up measurer and forwarding arguments,
770     the main business is to translate min/max rate from unidir to aggregate.
771     """
772
773     @staticmethod
774     def perform_optimized_ndrpdr_search(
775             frame_size, traffic_profile, minimum_transmit_rate,
776             maximum_transmit_rate, packet_loss_ratio=0.005,
777             final_relative_width=0.005, final_trial_duration=30.0,
778             initial_trial_duration=1.0, number_of_intermediate_phases=2,
779             timeout=720.0, doublings=1, traffic_directions=2):
780         """Setup initialized TG, perform optimized search, return intervals.
781
782         :param frame_size: Frame size identifier or value [B].
783         :param traffic_profile: Module name as a traffic profile identifier.
784             See resources/traffic_profiles/trex for implemented modules.
785         :param minimum_transmit_rate: Minimal uni-directional
786             target transmit rate [pps].
787         :param maximum_transmit_rate: Maximal uni-directional
788             target transmit rate [pps].
789         :param packet_loss_ratio: Fraction of packets lost, for PDR [1].
790         :param final_relative_width: Final lower bound transmit rate
791             cannot be more distant that this multiple of upper bound [1].
792         :param final_trial_duration: Trial duration for the final phase [s].
793         :param initial_trial_duration: Trial duration for the initial phase
794             and also for the first intermediate phase [s].
795         :param number_of_intermediate_phases: Number of intermediate phases
796             to perform before the final phase [1].
797         :param timeout: The search will fail itself when not finished
798             before this overall time [s].
799         :param doublings: How many doublings to do in external search step.
800             Default 1 is suitable for fairly stable tests,
801             less stable tests might get better overal duration with 2 or more.
802         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
803             Default: 2
804         :type frame_size: str or int
805         :type traffic_profile: str
806         :type minimum_transmit_rate: float
807         :type maximum_transmit_rate: float
808         :type packet_loss_ratio: float
809         :type final_relative_width: float
810         :type final_trial_duration: float
811         :type initial_trial_duration: float
812         :type number_of_intermediate_phases: int
813         :type timeout: float
814         :type doublings: int
815         :type traffic_directions: int
816         :returns: Structure containing narrowed down NDR and PDR intervals
817             and their measurements.
818         :rtype: NdrPdrResult
819         :raises RuntimeError: If total duration is larger than timeout.
820         """
821         minimum_transmit_rate *= traffic_directions
822         maximum_transmit_rate *= traffic_directions
823         # we need instance of TrafficGenerator instantiated by Robot Framework
824         # to be able to use trex_stl-*()
825         tg_instance = BuiltIn().get_library_instance(
826             u"resources.libraries.python.TrafficGenerator"
827         )
828         tg_instance.set_rate_provider_defaults(
829             frame_size, traffic_profile, traffic_directions=traffic_directions)
830         algorithm = MultipleLossRatioSearch(
831             measurer=tg_instance, final_trial_duration=final_trial_duration,
832             final_relative_width=final_relative_width,
833             number_of_intermediate_phases=number_of_intermediate_phases,
834             initial_trial_duration=initial_trial_duration, timeout=timeout,
835             doublings=doublings
836         )
837         result = algorithm.narrow_down_ndr_and_pdr(
838             minimum_transmit_rate, maximum_transmit_rate, packet_loss_ratio
839         )
840         return result
841
842     @staticmethod
843     def perform_soak_search(
844             frame_size, traffic_profile, minimum_transmit_rate,
845             maximum_transmit_rate, plr_target=1e-7, tdpt=0.1,
846             initial_count=50, timeout=1800.0, trace_enabled=False,
847             traffic_directions=2):
848         """Setup initialized TG, perform soak search, return avg and stdev.
849
850         :param frame_size: Frame size identifier or value [B].
851         :param traffic_profile: Module name as a traffic profile identifier.
852             See resources/traffic_profiles/trex for implemented modules.
853         :param minimum_transmit_rate: Minimal uni-directional
854             target transmit rate [pps].
855         :param maximum_transmit_rate: Maximal uni-directional
856             target transmit rate [pps].
857         :param plr_target: Fraction of packets lost to achieve [1].
858         :param tdpt: Trial duration per trial.
859             The algorithm linearly increases trial duration with trial number,
860             this is the increment between succesive trials, in seconds.
861         :param initial_count: Offset to apply before the first trial.
862             For example initial_count=50 makes first trial to be 51*tdpt long.
863             This is needed because initial "search" phase of integrator
864             takes significant time even without any trial results.
865         :param timeout: The search will stop after this overall time [s].
866         :param trace_enabled: True if trace enabled else False.
867         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
868             Default: 2
869         :type frame_size: str or int
870         :type traffic_profile: str
871         :type minimum_transmit_rate: float
872         :type maximum_transmit_rate: float
873         :type plr_target: float
874         :type initial_count: int
875         :type timeout: float
876         :type trace_enabled: bool
877         :type traffic_directions: int
878         :returns: Average and stdev of estimated aggregate rate giving PLR.
879         :rtype: 2-tuple of float
880         """
881         minimum_transmit_rate *= traffic_directions
882         maximum_transmit_rate *= traffic_directions
883         tg_instance = BuiltIn().get_library_instance(
884             u"resources.libraries.python.TrafficGenerator"
885         )
886         tg_instance.set_rate_provider_defaults(
887             frame_size, traffic_profile, traffic_directions=traffic_directions)
888         algorithm = PLRsearch(
889             measurer=tg_instance, trial_duration_per_trial=tdpt,
890             packet_loss_ratio_target=plr_target,
891             trial_number_offset=initial_count, timeout=timeout,
892             trace_enabled=trace_enabled
893         )
894         result = algorithm.search(minimum_transmit_rate, maximum_transmit_rate)
895         return result