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