STL traffic: Use the sleep+stop logic
[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:
1311             logger.debug(f"Unsent packets/transactions: {unsent}")
1312         if fail_count < 0 and not self.negative_loss:
1313             fail_count = 0
1314         measurement = ReceiveRateMeasurement(
1315             duration=target_duration,
1316             target_tr=transmit_rate,
1317             transmit_count=expected_attempt_count,
1318             loss_count=fail_count,
1319             approximated_duration=approximated_duration,
1320             partial_transmit_count=partial_attempt_count,
1321         )
1322         measurement.latency = self.get_latency_int()
1323         return measurement
1324
1325     def measure(self, duration, transmit_rate):
1326         """Run trial measurement, parse and return results.
1327
1328         The input rate is for transactions. Stateles bidirectional traffic
1329         is understood as sequence of (asynchronous) transactions,
1330         two packets each.
1331
1332         The result units depend on test type, generally
1333         the count either transactions or packets (aggregated over directions).
1334
1335         Optionally, this method sleeps if measurement finished before
1336         the time specified as duration.
1337
1338         :param duration: Trial duration [s].
1339         :param transmit_rate: Target rate in transactions per second.
1340         :type duration: float
1341         :type transmit_rate: float
1342         :returns: Structure containing the result of the measurement.
1343         :rtype: ReceiveRateMeasurement
1344         :raises RuntimeError: If TG is not set or if node is not TG
1345             or if subtype is not specified.
1346         :raises NotImplementedError: If TG is not supported.
1347         """
1348         duration = float(duration)
1349         time_start = time.monotonic()
1350         time_stop = time_start + duration
1351         if self.resetter:
1352             self.resetter()
1353         result = self._send_traffic_on_tg_with_ramp_up(
1354             duration=duration,
1355             rate=transmit_rate,
1356             async_call=False,
1357         )
1358         logger.debug(f"trial measurement result: {result!r}")
1359         # In PLRsearch, computation needs the specified time to complete.
1360         if self.sleep_till_duration:
1361             sleeptime = time_stop - time.monotonic()
1362             if sleeptime > 0.0:
1363                 # TODO: Sometimes we have time to do additional trials here,
1364                 # adapt PLRsearch to accept all the results.
1365                 time.sleep(sleeptime)
1366         return result
1367
1368     def set_rate_provider_defaults(
1369             self,
1370             frame_size,
1371             traffic_profile,
1372             ppta=1,
1373             resetter=None,
1374             traffic_directions=2,
1375             transaction_duration=0.0,
1376             transaction_scale=0,
1377             transaction_type=u"packet",
1378             duration_limit=0.0,
1379             negative_loss=True,
1380             sleep_till_duration=False,
1381             use_latency=False,
1382             ramp_up_rate=None,
1383             ramp_up_duration=None,
1384             state_timeout=300.0,
1385         ):
1386         """Store values accessed by measure().
1387
1388         :param frame_size: Frame size identifier or value [B].
1389         :param traffic_profile: Module name as a traffic profile identifier.
1390             See GPL/traffic_profiles/trex for implemented modules.
1391         :param ppta: Packets per transaction, aggregated over directions.
1392             Needed for udp_pps which does not have a good transaction counter,
1393             so we need to compute expected number of packets.
1394             Default: 1.
1395         :param resetter: Callable to reset DUT state for repeated trials.
1396         :param traffic_directions: Traffic from packet counting point of view
1397             is bi- (2) or uni- (1) directional.
1398             Default: 2
1399         :param transaction_duration: Total expected time to close transaction.
1400         :param transaction_scale: Number of transactions to perform.
1401             0 (default) means unlimited.
1402         :param transaction_type: An identifier specifying which counters
1403             and formulas to use when computing attempted and failed
1404             transactions. Default: "packet".
1405             TODO: Does this also specify parsing for the measured duration?
1406         :param duration_limit: Zero or maximum limit for computed (or given)
1407             duration.
1408         :param negative_loss: If false, negative loss is reported as zero loss.
1409         :param sleep_till_duration: If true and measurement returned faster,
1410             sleep until it matches duration. Needed for PLRsearch.
1411         :param use_latency: Whether to measure latency during the trial.
1412             Default: False.
1413         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1414         :param ramp_up_duration: Duration of ramp-up trials [s].
1415         :param state_timeout: Time of life of DUT state [s].
1416         :type frame_size: str or int
1417         :type traffic_profile: str
1418         :type ppta: int
1419         :type resetter: Optional[Callable[[], None]]
1420         :type traffic_directions: int
1421         :type transaction_duration: float
1422         :type transaction_scale: int
1423         :type transaction_type: str
1424         :type duration_limit: float
1425         :type negative_loss: bool
1426         :type sleep_till_duration: bool
1427         :type use_latency: bool
1428         :type ramp_up_rate: float
1429         :type ramp_up_duration: float
1430         :type state_timeout: float
1431         """
1432         self.frame_size = frame_size
1433         self.traffic_profile = str(traffic_profile)
1434         self.resetter = resetter
1435         self.ppta = ppta
1436         self.traffic_directions = int(traffic_directions)
1437         self.transaction_duration = float(transaction_duration)
1438         self.transaction_scale = int(transaction_scale)
1439         self.transaction_type = str(transaction_type)
1440         self.duration_limit = float(duration_limit)
1441         self.negative_loss = bool(negative_loss)
1442         self.sleep_till_duration = bool(sleep_till_duration)
1443         self.use_latency = bool(use_latency)
1444         self.ramp_up_rate = float(ramp_up_rate)
1445         self.ramp_up_duration = float(ramp_up_duration)
1446         self.state_timeout = float(state_timeout)
1447
1448
1449 class OptimizedSearch:
1450     """Class to be imported as Robot Library, containing search keywords.
1451
1452     Aside of setting up measurer and forwarding arguments,
1453     the main business is to translate min/max rate from unidir to aggregate.
1454     """
1455
1456     @staticmethod
1457     def perform_optimized_ndrpdr_search(
1458             frame_size,
1459             traffic_profile,
1460             minimum_transmit_rate,
1461             maximum_transmit_rate,
1462             packet_loss_ratio=0.005,
1463             final_relative_width=0.005,
1464             final_trial_duration=30.0,
1465             initial_trial_duration=1.0,
1466             number_of_intermediate_phases=2,
1467             timeout=720.0,
1468             ppta=1,
1469             resetter=None,
1470             traffic_directions=2,
1471             transaction_duration=0.0,
1472             transaction_scale=0,
1473             transaction_type=u"packet",
1474             use_latency=False,
1475             ramp_up_rate=None,
1476             ramp_up_duration=None,
1477             state_timeout=300.0,
1478             expansion_coefficient=4.0,
1479     ):
1480         """Setup initialized TG, perform optimized search, return intervals.
1481
1482         If transaction_scale is nonzero, all init and non-init trial durations
1483         are set to 1.0 (as they do not affect the real trial duration)
1484         and zero intermediate phases are used.
1485         This way no re-measurement happens.
1486         Warmup has to be handled via resetter or ramp-up mechanisms.
1487
1488         :param frame_size: Frame size identifier or value [B].
1489         :param traffic_profile: Module name as a traffic profile identifier.
1490             See GPL/traffic_profiles/trex for implemented modules.
1491         :param minimum_transmit_rate: Minimal load in transactions per second.
1492         :param maximum_transmit_rate: Maximal load in transactions per second.
1493         :param packet_loss_ratio: Ratio of packets lost, for PDR [1].
1494         :param final_relative_width: Final lower bound transmit rate
1495             cannot be more distant that this multiple of upper bound [1].
1496         :param final_trial_duration: Trial duration for the final phase [s].
1497         :param initial_trial_duration: Trial duration for the initial phase
1498             and also for the first intermediate phase [s].
1499         :param number_of_intermediate_phases: Number of intermediate phases
1500             to perform before the final phase [1].
1501         :param timeout: The search will fail itself when not finished
1502             before this overall time [s].
1503         :param ppta: Packets per transaction, aggregated over directions.
1504             Needed for udp_pps which does not have a good transaction counter,
1505             so we need to compute expected number of packets.
1506             Default: 1.
1507         :param resetter: Callable to reset DUT state for repeated trials.
1508         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1509             Default: 2
1510         :param transaction_duration: Total expected time to close transaction.
1511         :param transaction_scale: Number of transactions to perform.
1512             0 (default) means unlimited.
1513         :param transaction_type: An identifier specifying which counters
1514             and formulas to use when computing attempted and failed
1515             transactions. Default: "packet".
1516         :param use_latency: Whether to measure latency during the trial.
1517             Default: False.
1518         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1519         :param ramp_up_duration: Duration of ramp-up trials [s].
1520         :param state_timeout: Time of life of DUT state [s].
1521         :param expansion_coefficient: In external search multiply width by this.
1522         :type frame_size: str or int
1523         :type traffic_profile: str
1524         :type minimum_transmit_rate: float
1525         :type maximum_transmit_rate: float
1526         :type packet_loss_ratio: float
1527         :type final_relative_width: float
1528         :type final_trial_duration: float
1529         :type initial_trial_duration: float
1530         :type number_of_intermediate_phases: int
1531         :type timeout: float
1532         :type ppta: int
1533         :type resetter: Optional[Callable[[], None]]
1534         :type traffic_directions: int
1535         :type transaction_duration: float
1536         :type transaction_scale: int
1537         :type transaction_type: str
1538         :type use_latency: bool
1539         :type ramp_up_rate: float
1540         :type ramp_up_duration: float
1541         :type state_timeout: float
1542         :type expansion_coefficient: float
1543         :returns: Structure containing narrowed down NDR and PDR intervals
1544             and their measurements.
1545         :rtype: List[Receiverateinterval]
1546         :raises RuntimeError: If total duration is larger than timeout.
1547         """
1548         # we need instance of TrafficGenerator instantiated by Robot Framework
1549         # to be able to use trex_stl-*()
1550         tg_instance = BuiltIn().get_library_instance(
1551             u"resources.libraries.python.TrafficGenerator"
1552         )
1553         # Overrides for fixed transaction amount.
1554         # TODO: Move to robot code? We have two call sites, so this saves space,
1555         #       even though this is surprising for log readers.
1556         if transaction_scale:
1557             initial_trial_duration = 1.0
1558             final_trial_duration = 1.0
1559             number_of_intermediate_phases = 0
1560             timeout += transaction_scale * 3e-4
1561         tg_instance.set_rate_provider_defaults(
1562             frame_size=frame_size,
1563             traffic_profile=traffic_profile,
1564             sleep_till_duration=False,
1565             ppta=ppta,
1566             resetter=resetter,
1567             traffic_directions=traffic_directions,
1568             transaction_duration=transaction_duration,
1569             transaction_scale=transaction_scale,
1570             transaction_type=transaction_type,
1571             use_latency=use_latency,
1572             ramp_up_rate=ramp_up_rate,
1573             ramp_up_duration=ramp_up_duration,
1574             state_timeout=state_timeout,
1575         )
1576         algorithm = MultipleLossRatioSearch(
1577             measurer=tg_instance,
1578             final_trial_duration=final_trial_duration,
1579             final_relative_width=final_relative_width,
1580             number_of_intermediate_phases=number_of_intermediate_phases,
1581             initial_trial_duration=initial_trial_duration,
1582             timeout=timeout,
1583             debug=logger.debug,
1584             expansion_coefficient=expansion_coefficient,
1585         )
1586         if packet_loss_ratio:
1587             packet_loss_ratios = [0.0, packet_loss_ratio]
1588         else:
1589             # Happens in reconf tests.
1590             packet_loss_ratios = [packet_loss_ratio]
1591         results = algorithm.narrow_down_intervals(
1592             min_rate=minimum_transmit_rate,
1593             max_rate=maximum_transmit_rate,
1594             packet_loss_ratios=packet_loss_ratios,
1595         )
1596         return results
1597
1598     @staticmethod
1599     def perform_soak_search(
1600             frame_size,
1601             traffic_profile,
1602             minimum_transmit_rate,
1603             maximum_transmit_rate,
1604             plr_target=1e-7,
1605             tdpt=0.1,
1606             initial_count=50,
1607             timeout=7200.0,
1608             ppta=1,
1609             resetter=None,
1610             trace_enabled=False,
1611             traffic_directions=2,
1612             transaction_duration=0.0,
1613             transaction_scale=0,
1614             transaction_type=u"packet",
1615             use_latency=False,
1616             ramp_up_rate=None,
1617             ramp_up_duration=None,
1618             state_timeout=300.0,
1619     ):
1620         """Setup initialized TG, perform soak search, return avg and stdev.
1621
1622         :param frame_size: Frame size identifier or value [B].
1623         :param traffic_profile: Module name as a traffic profile identifier.
1624             See GPL/traffic_profiles/trex for implemented modules.
1625         :param minimum_transmit_rate: Minimal load in transactions per second.
1626         :param maximum_transmit_rate: Maximal load in transactions per second.
1627         :param plr_target: Ratio of packets lost to achieve [1].
1628         :param tdpt: Trial duration per trial.
1629             The algorithm linearly increases trial duration with trial number,
1630             this is the increment between succesive trials, in seconds.
1631         :param initial_count: Offset to apply before the first trial.
1632             For example initial_count=50 makes first trial to be 51*tdpt long.
1633             This is needed because initial "search" phase of integrator
1634             takes significant time even without any trial results.
1635         :param timeout: The search will stop after this overall time [s].
1636         :param ppta: Packets per transaction, aggregated over directions.
1637             Needed for udp_pps which does not have a good transaction counter,
1638             so we need to compute expected number of packets.
1639             Default: 1.
1640         :param resetter: Callable to reset DUT state for repeated trials.
1641         :param trace_enabled: True if trace enabled else False.
1642             This is very verbose tracing on numeric computations,
1643             do not use in production.
1644             Default: False
1645         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1646             Default: 2
1647         :param transaction_duration: Total expected time to close transaction.
1648         :param transaction_scale: Number of transactions to perform.
1649             0 (default) means unlimited.
1650         :param transaction_type: An identifier specifying which counters
1651             and formulas to use when computing attempted and failed
1652             transactions. Default: "packet".
1653         :param use_latency: Whether to measure latency during the trial.
1654             Default: False.
1655         :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1656         :param ramp_up_duration: Duration of ramp-up trials [s].
1657         :param state_timeout: Time of life of DUT state [s].
1658         :type frame_size: str or int
1659         :type traffic_profile: str
1660         :type minimum_transmit_rate: float
1661         :type maximum_transmit_rate: float
1662         :type plr_target: float
1663         :type initial_count: int
1664         :type timeout: float
1665         :type ppta: int
1666         :type resetter: Optional[Callable[[], None]]
1667         :type trace_enabled: bool
1668         :type traffic_directions: int
1669         :type transaction_duration: float
1670         :type transaction_scale: int
1671         :type transaction_type: str
1672         :type use_latency: bool
1673         :type ramp_up_rate: float
1674         :type ramp_up_duration: float
1675         :type state_timeout: float
1676         :returns: Average and stdev of estimated aggregate rate giving PLR.
1677         :rtype: 2-tuple of float
1678         """
1679         tg_instance = BuiltIn().get_library_instance(
1680             u"resources.libraries.python.TrafficGenerator"
1681         )
1682         # Overrides for fixed transaction amount.
1683         # TODO: Move to robot code? We have a single call site
1684         #       but MLRsearch has two and we want the two to be used similarly.
1685         if transaction_scale:
1686             # TODO: What is a good value for max scale?
1687             # TODO: Scale the timeout with transaction scale.
1688             timeout = 7200.0
1689         tg_instance.set_rate_provider_defaults(
1690             frame_size=frame_size,
1691             traffic_profile=traffic_profile,
1692             negative_loss=False,
1693             sleep_till_duration=True,
1694             ppta=ppta,
1695             resetter=resetter,
1696             traffic_directions=traffic_directions,
1697             transaction_duration=transaction_duration,
1698             transaction_scale=transaction_scale,
1699             transaction_type=transaction_type,
1700             use_latency=use_latency,
1701             ramp_up_rate=ramp_up_rate,
1702             ramp_up_duration=ramp_up_duration,
1703             state_timeout=state_timeout,
1704         )
1705         algorithm = PLRsearch(
1706             measurer=tg_instance,
1707             trial_duration_per_trial=tdpt,
1708             packet_loss_ratio_target=plr_target,
1709             trial_number_offset=initial_count,
1710             timeout=timeout,
1711             trace_enabled=trace_enabled,
1712         )
1713         result = algorithm.search(
1714             min_rate=minimum_transmit_rate,
1715             max_rate=maximum_transmit_rate,
1716         )
1717         return result