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