1 # Copyright (c) 2022 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:
6 # http://www.apache.org/licenses/LICENSE-2.0
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.
14 """Performance testing traffic generator library."""
19 from robot.api import logger
20 from robot.libraries.BuiltIn import BuiltIn
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
35 __all__ = [u"TGDropRateSearchImpl", u"TrafficGenerator", u"OptimizedSearch"]
38 def check_subtype(node):
39 """Return supported subtype of given node, or raise an exception.
41 Currently only one subtype is supported,
42 but we want our code to be ready for other ones.
44 :param node: Topology node to check. Can be None.
45 :type node: dict or NoneType
46 :returns: Subtype detected.
48 :raises RuntimeError: If node is not supported, message explains how.
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"
59 return NodeSubTypeTG.TREX
60 raise RuntimeError(msg)
63 class TGDropRateSearchImpl(DropRateSearch):
64 """Drop Rate Search implementation."""
67 # super(TGDropRateSearchImpl, self).__init__()
70 self, rate, frame_size, loss_acceptance, loss_acceptance_type,
72 """Runs the traffic and evaluate the measured results.
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.
82 :type loss_acceptance: float
83 :type loss_acceptance_type: LossAcceptanceType
84 :type traffic_profile: str
85 :returns: Drop threshold exceeded? (True/False)
87 :raises NotImplementedError: If TG is not supported.
88 :raises RuntimeError: If TG is not specified.
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"
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
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
106 f"comparing: {loss} < {loss_acceptance} {loss_acceptance_type}"
108 return float(loss) <= float(loss_acceptance)
111 def get_latency(self):
112 """Returns min/avg/max latency.
114 :returns: Latency stats.
117 tg_instance = BuiltIn().get_library_instance(
118 u"resources.libraries.python.TrafficGenerator"
120 return tg_instance.get_latency_int()
124 """Defines mode of T-Rex traffic generator."""
125 # Advanced stateful mode
131 # TODO: Pylint says too-many-instance-attributes.
132 class TrafficGenerator(AbstractMeasurer):
133 """Traffic Generator."""
135 # TODO: Remove "trex" from lines which could work with other TGs.
137 # Use one instance of TrafficGenerator for all tests in test suite
138 ROBOT_LIBRARY_SCOPE = u"TEST SUITE"
141 # TODO: Separate into few dataclasses/dicts.
142 # Pylint dislikes large unstructured state, and it is right.
145 # TG interface order mapping
146 self._ifaces_reordered = False
147 # Result holding fields, to be removed.
152 self._received = None
153 self._approximated_rate = None
154 self._approximated_duration = None
156 # Measurement input fields, needed for async stop result.
157 self._start_time = None
158 self._stop_time = 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
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?
188 :returns: Traffic generator node.
194 """Return number of lost packets.
196 :returns: Number of lost packets.
202 """Return number of sent packets.
204 :returns: Number of sent packets.
209 def get_received(self):
210 """Return number of received packets.
212 :returns: Number of received packets.
215 return self._received
217 def get_latency_int(self):
218 """Return rounded min/avg/max latency.
220 :returns: Latency stats.
225 def get_approximated_rate(self):
226 """Return approximated rate computed as ratio of transmitted packets
227 over duration of trial.
229 :returns: Approximated rate.
232 return self._approximated_rate
234 def get_l7_data(self):
237 :returns: Number of received packets.
242 def check_mode(self, expected_mode):
245 :param expected_mode: Expected traffic generator mode.
246 :type expected_mode: object
247 :raises RuntimeError: In case of unexpected TG mode.
249 if self._mode == expected_mode:
252 f"{self._node[u'subtype']} not running in {expected_mode} mode!"
256 def get_tg_type(tg_node):
257 """Log and return the installed traffic generator type.
259 :param tg_node: Node from topology file.
261 :returns: Traffic generator type string.
263 :raises RuntimeError: If command returns nonzero return code.
265 return str(check_subtype(tg_node))
268 def get_tg_version(tg_node):
269 """Log and return the installed traffic generator version.
271 :param tg_node: Node from topology file.
273 :returns: Traffic generator version string.
275 :raises RuntimeError: If command returns nonzero return code.
277 subtype = check_subtype(tg_node)
278 if subtype == NodeSubTypeTG.TREX:
279 command = f"cat {Constants.TREX_INSTALL_DIR}/VERSION"
280 message = u"Get T-Rex version failed!"
281 stdout, _ = exec_cmd_no_error(tg_node, command, message=message)
282 return stdout.strip()
286 # TODO: pylint disable=too-many-locals.
287 def initialize_traffic_generator(
288 self, tg_node, tg_if1, tg_if2, tg_if1_adj_node, tg_if1_adj_if,
289 tg_if2_adj_node, tg_if2_adj_if, osi_layer, tg_if1_dst_mac=None,
290 tg_if2_dst_mac=None):
291 """TG initialization.
293 TODO: Document why do we need (and how do we use) _ifaces_reordered.
295 :param tg_node: Traffic generator node.
296 :param tg_if1: TG - name of first interface.
297 :param tg_if2: TG - name of second interface.
298 :param tg_if1_adj_node: TG if1 adjecent node.
299 :param tg_if1_adj_if: TG if1 adjecent interface.
300 :param tg_if2_adj_node: TG if2 adjecent node.
301 :param tg_if2_adj_if: TG if2 adjecent interface.
302 :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
303 :param tg_if1_dst_mac: Interface 1 destination MAC address.
304 :param tg_if2_dst_mac: Interface 2 destination MAC address.
308 :type tg_if1_adj_node: dict
309 :type tg_if1_adj_if: str
310 :type tg_if2_adj_node: dict
311 :type tg_if2_adj_if: str
313 :type tg_if1_dst_mac: str
314 :type tg_if2_dst_mac: str
316 :raises RuntimeError: In case of issue during initialization.
318 subtype = check_subtype(tg_node)
319 if subtype == NodeSubTypeTG.TREX:
321 self._mode = TrexMode.ASTF if osi_layer == u"L7" else TrexMode.STL
324 if1[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if1)
325 if2[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if2)
326 if1[u"addr"] = Topology().get_interface_mac(self._node, tg_if1)
327 if2[u"addr"] = Topology().get_interface_mac(self._node, tg_if2)
329 if osi_layer == u"L2":
330 if1[u"adj_addr"] = if2[u"addr"]
331 if2[u"adj_addr"] = if1[u"addr"]
332 elif osi_layer in (u"L3", u"L7"):
333 if1[u"adj_addr"] = Topology().get_interface_mac(
334 tg_if1_adj_node, tg_if1_adj_if
336 if2[u"adj_addr"] = Topology().get_interface_mac(
337 tg_if2_adj_node, tg_if2_adj_if
340 raise ValueError(u"Unknown OSI layer!")
342 # in case of switched environment we can override MAC addresses
343 if tg_if1_dst_mac is not None and tg_if2_dst_mac is not None:
344 if1[u"adj_addr"] = tg_if1_dst_mac
345 if2[u"adj_addr"] = tg_if2_dst_mac
347 if min(if1[u"pci"], if2[u"pci"]) != if1[u"pci"]:
349 self._ifaces_reordered = True
351 master_thread_id, latency_thread_id, socket, threads = \
352 CpuUtils.get_affinity_trex(
353 self._node, tg_if1, tg_if2,
354 tg_dtc=Constants.TREX_CORE_COUNT)
356 if osi_layer in (u"L2", u"L3", u"L7"):
359 f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
361 f" c: {len(threads)}\n"
362 f" limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
363 f" interfaces: [\"{if1[u'pci']}\",\"{if2[u'pci']}\"]\n"
365 f" - dest_mac: \'{if1[u'adj_addr']}\'\n"
366 f" src_mac: \'{if1[u'addr']}\'\n"
367 f" - dest_mac: \'{if2[u'adj_addr']}\'\n"
368 f" src_mac: \'{if2[u'addr']}\'\n"
370 f" master_thread_id: {master_thread_id}\n"
371 f" latency_thread_id: {latency_thread_id}\n"
373 f" - socket: {socket}\n"
374 f" threads: {threads}\n"
376 sudo=True, message=u"T-Rex config generation!"
379 if Constants.TREX_RX_DESCRIPTORS_COUNT != 0:
382 f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
383 f" rx_desc: {Constants.TREX_RX_DESCRIPTORS_COUNT}\n"
385 sudo=True, message=u"T-Rex rx_desc modification!"
388 if Constants.TREX_TX_DESCRIPTORS_COUNT != 0:
391 f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
392 f" tx_desc: {Constants.TREX_TX_DESCRIPTORS_COUNT}\n"
394 sudo=True, message=u"T-Rex tx_desc modification!"
397 raise ValueError(u"Unknown OSI layer!")
399 TrafficGenerator.startup_trex(
400 self._node, osi_layer, subtype=subtype
404 def startup_trex(tg_node, osi_layer, subtype=None):
405 """Startup sequence for the TRex traffic generator.
407 :param tg_node: Traffic generator node.
408 :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
409 :param subtype: Traffic generator sub-type.
412 :type subtype: NodeSubTypeTG
413 :raises RuntimeError: If T-Rex startup failed.
414 :raises ValueError: If OSI layer is not supported.
417 subtype = check_subtype(tg_node)
418 if subtype == NodeSubTypeTG.TREX:
419 for _ in range(0, 3):
420 # Kill TRex only if it is already running.
421 cmd = u"sh -c \"pgrep t-rex && pkill t-rex && sleep 3 || true\""
423 tg_node, cmd, sudo=True, message=u"Kill TRex failed!"
426 # Prepare interfaces for TRex.
430 for port in tg_node[u"interfaces"].values():
431 if u"Mellanox" in port.get(u"model"):
432 mlx_ports += f" {port.get(u'pci_address')}"
433 mlx_driver = port.get(u"driver")
434 if u"Intel" in port.get(u"model"):
435 itl_ports += f" {port.get(u'pci_address')}"
439 f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && ",
440 f"./dpdk_nic_bind.py -u {itl_ports} || ",
444 tg_node, cmd, sudo=True,
445 message=u"Unbind PCI ports from driver failed!"
449 f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && ",
450 f"./dpdk_nic_bind.py -b {mlx_driver} {mlx_ports} || ",
454 tg_node, cmd, sudo=True,
455 message=u"Bind PCI ports from driver failed!"
459 cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
460 trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
462 trex_cmd.add(u"--prefix $(hostname)")
463 trex_cmd.add(u"--hdrh")
464 trex_cmd.add(u"--no-scapy-server")
465 trex_cmd.add_if(u"--astf", osi_layer == u"L7")
466 # OptionString does not create double space if extra is empty.
467 trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
468 inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
469 cmd = f"sh -c \"{inner_command}\" > /dev/null"
471 exec_cmd_no_error(tg_node, cmd, sudo=True)
473 cmd = u"sh -c \"cat /tmp/trex.log\""
475 tg_node, cmd, sudo=True,
476 message=u"Get TRex logs failed!"
478 raise RuntimeError(u"Start TRex failed!")
480 # Test T-Rex API responsiveness.
481 cmd = f"python3 {Constants.REMOTE_FW_DIR}/GPL/tools/trex/"
482 if osi_layer in (u"L2", u"L3"):
483 cmd += u"trex_stl_assert.py"
484 elif osi_layer == u"L7":
485 cmd += u"trex_astf_assert.py"
487 raise ValueError(u"Unknown OSI layer!")
490 tg_node, cmd, sudo=True,
491 message=u"T-Rex API is not responding!", retries=20
496 # After max retries TRex is still not responding to API critical
498 exec_cmd(tg_node, u"cat /tmp/trex.log", sudo=True)
499 raise RuntimeError(u"Start T-Rex failed after multiple retries!")
502 def is_trex_running(node):
503 """Check if T-Rex is running using pidof.
505 :param node: Traffic generator node.
507 :returns: True if T-Rex is running otherwise False.
510 ret, _, _ = exec_cmd(node, u"pgrep t-rex", sudo=True)
511 return bool(int(ret) == 0)
514 def teardown_traffic_generator(node):
517 :param node: Traffic generator node.
520 :raises RuntimeError: If node type is not a TG,
521 or if T-Rex teardown fails.
523 subtype = check_subtype(node)
524 if subtype == NodeSubTypeTG.TREX:
528 u"\"if pgrep t-rex; then sudo pkill t-rex && sleep 3; fi\"",
530 message=u"T-Rex kill failed!"
533 def trex_astf_stop_remote_exec(self, node):
534 """Execute T-Rex ASTF script on remote node over ssh to stop running
537 Internal state is updated with measurement results.
539 :param node: T-Rex generator node.
541 :raises RuntimeError: If stop traffic script fails.
543 command_line = OptionString().add(u"python3")
544 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
545 command_line.add(f"'{dirname}/trex_astf_stop.py'")
546 command_line.change_prefix(u"--")
547 for index, value in enumerate(self._xstats):
548 if value is not None:
549 value = value.replace(u"'", u"\"")
550 command_line.add_equals(f"xstat{index}", f"'{value}'")
551 stdout, _ = exec_cmd_no_error(
553 message=u"T-Rex ASTF runtime error!"
555 self._parse_traffic_results(stdout)
557 def trex_stl_stop_remote_exec(self, node):
558 """Execute T-Rex STL script on remote node over ssh to stop running
561 Internal state is updated with measurement results.
563 :param node: T-Rex generator node.
565 :raises RuntimeError: If stop traffic script fails.
567 command_line = OptionString().add(u"python3")
568 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
569 command_line.add(f"'{dirname}/trex_stl_stop.py'")
570 command_line.change_prefix(u"--")
571 for index, value in enumerate(self._xstats):
572 if value is not None:
573 value = value.replace(u"'", u"\"")
574 command_line.add_equals(f"xstat{index}", f"'{value}'")
575 stdout, _ = exec_cmd_no_error(
577 message=u"T-Rex STL runtime error!"
579 self._parse_traffic_results(stdout)
581 def stop_traffic_on_tg(self):
582 """Stop all traffic on TG.
584 :returns: Structure containing the result of the measurement.
585 :rtype: ReceiveRateMeasurement
586 :raises ValueError: If TG traffic profile is not supported.
588 subtype = check_subtype(self._node)
589 if subtype != NodeSubTypeTG.TREX:
590 raise ValueError(f"Unsupported TG subtype: {subtype!r}")
591 if u"trex-astf" in self.traffic_profile:
592 self.trex_astf_stop_remote_exec(self._node)
593 elif u"trex-stl" in self.traffic_profile:
594 self.trex_stl_stop_remote_exec(self._node)
596 raise ValueError(u"Unsupported T-Rex traffic profile!")
597 self._stop_time = time.monotonic()
599 return self._get_measurement_result()
601 def _compute_duration(self, duration, multiplier):
602 """Compute duration for profile driver.
604 The final result is influenced by transaction scale and duration limit.
605 It is assumed a higher level function has already set those to self.
606 The duration argument is the target value from search point of view,
607 before the overrides are applied here.
609 Minus one (signalling async traffic start) is kept.
611 Completeness flag is also included. Duration limited or async trials
612 are not considered complete for ramp-up purposes.
614 :param duration: Time expressed in seconds for how long to send traffic.
615 :param multiplier: Traffic rate in transactions per second.
616 :type duration: float
617 :type multiplier: float
618 :returns: New duration and whether it was a complete ramp-up candidate.
623 return duration, False
624 computed_duration = duration
625 if self.transaction_scale:
626 computed_duration = self.transaction_scale / multiplier
627 # Log the computed duration,
628 # so we can compare with what telemetry suggests
629 # the real duration was.
630 logger.debug(f"Expected duration {computed_duration}")
631 if not self.duration_limit:
632 return computed_duration, True
633 limited_duration = min(computed_duration, self.duration_limit)
634 return limited_duration, (limited_duration == computed_duration)
636 def trex_astf_start_remote_exec(
637 self, duration, multiplier, async_call=False):
638 """Execute T-Rex ASTF script on remote node over ssh to start running
641 In sync mode, measurement results are stored internally.
642 In async mode, initial data including xstats are stored internally.
644 This method contains the logic to compute duration as maximum time
645 if transaction_scale is nonzero.
646 The transaction_scale argument defines (limits) how many transactions
647 will be started in total. As that amount of transaction can take
648 considerable time (sometimes due to explicit delays in the profile),
649 the real time a trial needs to finish is computed here. For now,
650 in that case the duration argument is ignored, assuming it comes
651 from ASTF-unaware search algorithm. The overall time a single
652 transaction needs is given in parameter transaction_duration,
653 it includes both explicit delays and implicit time it takes
654 to transfer data (or whatever the transaction does).
656 Currently it is observed TRex does not start the ASTF traffic
657 immediately, an ad-hoc constant is added to the computed duration
658 to compensate for that.
660 If transaction_scale is zero, duration is not recomputed.
661 It is assumed the subsequent result parsing gets the real duration
662 if the traffic stops sooner for any reason.
664 Currently, it is assumed traffic profile defines a single transaction.
665 To avoid heavy logic here, the input rate is expected to be in
666 transactions per second, as that directly translates to TRex multiplier,
667 (assuming the profile does not override the default cps value of one).
669 :param duration: Time expressed in seconds for how long to send traffic.
670 :param multiplier: Traffic rate in transactions per second.
671 :param async_call: If enabled then don't wait for all incoming traffic.
672 :type duration: float
673 :type multiplier: int
674 :type async_call: bool
675 :raises RuntimeError: In case of T-Rex driver issue.
677 self.check_mode(TrexMode.ASTF)
678 p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
679 if not isinstance(duration, (float, int)):
680 duration = float(duration)
682 # TODO: Refactor the code so duration is computed only once,
683 # and both the initial and the computed durations are logged.
684 computed_duration, _ = self._compute_duration(duration, multiplier)
686 command_line = OptionString().add(u"python3")
687 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
688 command_line.add(f"'{dirname}/trex_astf_profile.py'")
689 command_line.change_prefix(u"--")
690 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
691 command_line.add_with_value(
692 u"profile", f"'{dirname}/{self.traffic_profile}.py'"
694 command_line.add_with_value(u"duration", f"{computed_duration!r}")
695 command_line.add_with_value(u"frame_size", self.frame_size)
696 command_line.add_with_value(
697 u"n_data_frames", Constants.ASTF_N_DATA_FRAMES
699 command_line.add_with_value(u"multiplier", multiplier)
700 command_line.add_with_value(u"port_0", p_0)
701 command_line.add_with_value(u"port_1", p_1)
702 command_line.add_with_value(
703 u"traffic_directions", self.traffic_directions
705 command_line.add_if(u"async_start", async_call)
706 command_line.add_if(u"latency", self.use_latency)
707 command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
708 command_line.add_with_value(
709 u"delay", Constants.PERF_TRIAL_ASTF_DELAY
712 self._start_time = time.monotonic()
713 self._rate = multiplier
714 stdout, _ = exec_cmd_no_error(
715 self._node, command_line, timeout=computed_duration + 10.0,
716 message=u"T-Rex ASTF runtime error!"
721 self._target_duration = None
722 self._duration = None
723 self._received = None
727 xstats = [None, None]
728 self._l7_data = dict()
729 self._l7_data[u"client"] = dict()
730 self._l7_data[u"client"][u"active_flows"] = None
731 self._l7_data[u"client"][u"established_flows"] = None
732 self._l7_data[u"client"][u"traffic_duration"] = None
733 self._l7_data[u"server"] = dict()
734 self._l7_data[u"server"][u"active_flows"] = None
735 self._l7_data[u"server"][u"established_flows"] = None
736 self._l7_data[u"server"][u"traffic_duration"] = None
737 if u"udp" in self.traffic_profile:
738 self._l7_data[u"client"][u"udp"] = dict()
739 self._l7_data[u"client"][u"udp"][u"connects"] = None
740 self._l7_data[u"client"][u"udp"][u"closed_flows"] = None
741 self._l7_data[u"client"][u"udp"][u"err_cwf"] = None
742 self._l7_data[u"server"][u"udp"] = dict()
743 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = None
744 self._l7_data[u"server"][u"udp"][u"closed_flows"] = None
745 elif u"tcp" in self.traffic_profile:
746 self._l7_data[u"client"][u"tcp"] = dict()
747 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = None
748 self._l7_data[u"client"][u"tcp"][u"connects"] = None
749 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = None
750 self._l7_data[u"client"][u"tcp"][u"connattempt"] = None
751 self._l7_data[u"server"][u"tcp"] = dict()
752 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = None
753 self._l7_data[u"server"][u"tcp"][u"connects"] = None
754 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = None
756 logger.warn(u"Unsupported T-Rex ASTF traffic profile!")
758 for line in stdout.splitlines():
759 if f"Xstats snapshot {index}: " in line:
760 xstats[index] = line[19:]
764 self._xstats = tuple(xstats)
766 self._target_duration = duration
767 self._duration = computed_duration
768 self._parse_traffic_results(stdout)
770 def trex_stl_start_remote_exec(self, duration, rate, async_call=False):
771 """Execute T-Rex STL script on remote node over ssh to start running
774 In sync mode, measurement results are stored internally.
775 In async mode, initial data including xstats are stored internally.
777 Mode-unaware code (e.g. in search algorithms) works with transactions.
778 To keep the logic simple, multiplier is set to that value.
779 As bidirectional traffic profiles send packets in both directions,
780 they are treated as transactions with two packets (one per direction).
782 :param duration: Time expressed in seconds for how long to send traffic.
783 :param rate: Traffic rate in transactions per second.
784 :param async_call: If enabled then don't wait for all incoming traffic.
785 :type duration: float
787 :type async_call: bool
788 :raises RuntimeError: In case of T-Rex driver issue.
790 self.check_mode(TrexMode.STL)
791 p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
792 if not isinstance(duration, (float, int)):
793 duration = float(duration)
795 # TODO: Refactor the code so duration is computed only once,
796 # and both the initial and the computed durations are logged.
797 duration, _ = self._compute_duration(duration=duration, multiplier=rate)
799 command_line = OptionString().add(u"python3")
800 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
801 command_line.add(f"'{dirname}/trex_stl_profile.py'")
802 command_line.change_prefix(u"--")
803 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
804 command_line.add_with_value(
805 u"profile", f"'{dirname}/{self.traffic_profile}.py'"
807 command_line.add_with_value(u"duration", f"{duration!r}")
808 command_line.add_with_value(u"frame_size", self.frame_size)
809 command_line.add_with_value(u"rate", f"{rate!r}")
810 command_line.add_with_value(u"port_0", p_0)
811 command_line.add_with_value(u"port_1", p_1)
812 command_line.add_with_value(
813 u"traffic_directions", self.traffic_directions
815 command_line.add_if(u"async_start", async_call)
816 command_line.add_if(u"latency", self.use_latency)
817 command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
818 command_line.add_with_value(u"delay", Constants.PERF_TRIAL_STL_DELAY)
820 # TODO: This is ugly. Handle parsing better.
821 self._start_time = time.monotonic()
822 self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
823 stdout, _ = exec_cmd_no_error(
824 self._node, command_line, timeout=int(duration) + 60,
825 message=u"T-Rex STL runtime error"
830 self._target_duration = None
831 self._duration = None
832 self._received = None
837 xstats = [None, None]
839 for line in stdout.splitlines():
840 if f"Xstats snapshot {index}: " in line:
841 xstats[index] = line[19:]
845 self._xstats = tuple(xstats)
847 self._target_duration = duration
848 self._duration = duration
849 self._parse_traffic_results(stdout)
851 def send_traffic_on_tg(
859 traffic_directions=2,
860 transaction_duration=0.0,
862 transaction_type=u"packet",
866 ramp_up_duration=None,
870 """Send traffic from all configured interfaces on TG.
872 In async mode, xstats is stored internally,
873 to enable getting correct result when stopping the traffic.
874 In both modes, stdout is returned,
875 but _parse_traffic_results only works in sync output.
877 Note that traffic generator uses DPDK driver which might
878 reorder port numbers based on wiring and PCI numbering.
879 This method handles that, so argument values are invariant,
880 but you can see swapped valued in debug logs.
882 When transaction_scale is specified, the duration value is ignored
883 and the needed time is computed. For cases where this results in
884 to too long measurement (e.g. teardown trial with small rate),
885 duration_limit is applied (of non-zero), so the trial is stopped sooner.
887 Bidirectional STL profiles are treated as transactions with two packets.
889 The return value is None for async.
891 :param duration: Duration of test traffic generation in seconds.
892 :param rate: Traffic rate in transactions per second.
893 :param frame_size: Frame size (L2) in Bytes.
894 :param traffic_profile: Module name as a traffic profile identifier.
895 See GPL/traffic_profiles/trex for implemented modules.
896 :param async_call: Async mode.
897 :param ppta: Packets per transaction, aggregated over directions.
898 Needed for udp_pps which does not have a good transaction counter,
899 so we need to compute expected number of packets.
901 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
903 :param transaction_duration: Total expected time to close transaction.
904 :param transaction_scale: Number of transactions to perform.
905 0 (default) means unlimited.
906 :param transaction_type: An identifier specifying which counters
907 and formulas to use when computing attempted and failed
908 transactions. Default: "packet".
909 :param duration_limit: Zero or maximum limit for computed (or given)
911 :param use_latency: Whether to measure latency during the trial.
913 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
914 :param ramp_up_duration: Duration of ramp-up trials [s].
915 :param state_timeout: Time of life of DUT state [s].
916 :param ramp_up_only: If true, do not perform main trial measurement.
917 :type duration: float
919 :type frame_size: str
920 :type traffic_profile: str
921 :type async_call: bool
923 :type traffic_directions: int
924 :type transaction_duration: float
925 :type transaction_scale: int
926 :type transaction_type: str
927 :type duration_limit: float
928 :type use_latency: bool
929 :type ramp_up_rate: float
930 :type ramp_up_duration: float
931 :type state_timeout: float
932 :type ramp_up_only: bool
933 :returns: TG results.
934 :rtype: ReceiveRateMeasurement or None
935 :raises ValueError: If TG traffic profile is not supported.
937 self.set_rate_provider_defaults(
938 frame_size=frame_size,
939 traffic_profile=traffic_profile,
941 traffic_directions=traffic_directions,
942 transaction_duration=transaction_duration,
943 transaction_scale=transaction_scale,
944 transaction_type=transaction_type,
945 duration_limit=duration_limit,
946 use_latency=use_latency,
947 ramp_up_rate=ramp_up_rate,
948 ramp_up_duration=ramp_up_duration,
949 state_timeout=state_timeout,
951 return self._send_traffic_on_tg_with_ramp_up(
954 async_call=async_call,
955 ramp_up_only=ramp_up_only,
958 def _send_traffic_on_tg_internal(
959 self, duration, rate, async_call=False):
960 """Send traffic from all configured interfaces on TG.
962 This is an internal function, it assumes set_rate_provider_defaults
963 has been called to remember most values.
964 The reason why need to remember various values is that
965 the traffic can be asynchronous, and parsing needs those values.
966 The reason why this is is a separate function from the one
967 which calls set_rate_provider_defaults is that some search algorithms
968 need to specify their own values, and we do not want the measure call
969 to overwrite them with defaults.
971 This function is used both for automated ramp-up trials
972 and for explicitly called trials.
974 :param duration: Duration of test traffic generation in seconds.
975 :param rate: Traffic rate in transactions per second.
976 :param async_call: Async mode.
977 :type duration: float
979 :type async_call: bool
980 :returns: TG results.
981 :rtype: ReceiveRateMeasurement or None
982 :raises ValueError: If TG traffic profile is not supported.
984 subtype = check_subtype(self._node)
985 if subtype == NodeSubTypeTG.TREX:
986 if u"trex-astf" in self.traffic_profile:
987 self.trex_astf_start_remote_exec(
988 duration, float(rate), async_call
990 elif u"trex-stl" in self.traffic_profile:
991 unit_rate_str = str(rate) + u"pps"
992 # TODO: Suport transaction_scale et al?
993 self.trex_stl_start_remote_exec(
994 duration, unit_rate_str, async_call
997 raise ValueError(u"Unsupported T-Rex traffic profile!")
999 return None if async_call else self._get_measurement_result()
1001 def _send_traffic_on_tg_with_ramp_up(
1002 self, duration, rate, async_call=False, ramp_up_only=False):
1003 """Send traffic from all interfaces on TG, maybe after ramp-up.
1005 This is an internal function, it assumes set_rate_provider_defaults
1006 has been called to remember most values.
1007 The reason why need to remember various values is that
1008 the traffic can be asynchronous, and parsing needs those values.
1009 The reason why this is a separate function from the one
1010 which calls set_rate_provider_defaults is that some search algorithms
1011 need to specify their own values, and we do not want the measure call
1012 to overwrite them with defaults.
1014 If ramp-up tracking is detected, a computation is performed,
1015 and if state timeout is near, trial at ramp-up rate and duration
1016 is inserted before the main trial measurement.
1018 The ramp_up_only parameter forces a ramp-up without immediate
1019 trial measurement, which is useful in case self remembers
1020 a previous ramp-up trial that belongs to a different test (phase).
1022 Return None if trial is async or ramp-up only.
1024 :param duration: Duration of test traffic generation in seconds.
1025 :param rate: Traffic rate in transactions per second.
1026 :param async_call: Async mode.
1027 :param ramp_up_only: If true, do not perform main trial measurement.
1028 :type duration: float
1030 :type async_call: bool
1031 :type ramp_up_only: bool
1032 :returns: TG results.
1033 :rtype: ReceiveRateMeasurement or None
1034 :raises ValueError: If TG traffic profile is not supported.
1037 if self.ramp_up_rate:
1038 # Figure out whether we need to insert a ramp-up trial.
1039 # TODO: Give up on async_call=True?
1040 if ramp_up_only or self.ramp_up_start is None:
1041 # We never ramped up yet (at least not in this test case).
1042 ramp_up_needed = True
1044 # We ramped up before, but maybe it was too long ago.
1045 # Adding a constant overhead to be safe.
1046 time_now = time.monotonic() + 1.0
1047 computed_duration, complete = self._compute_duration(
1051 # There are two conditions for inserting ramp-up.
1052 # If early sessions are expiring already,
1053 # or if late sessions are to expire before measurement is over.
1054 ramp_up_start_delay = time_now - self.ramp_up_start
1055 ramp_up_stop_delay = time_now - self.ramp_up_stop
1056 ramp_up_stop_delay += computed_duration
1057 bigger_delay = max(ramp_up_start_delay, ramp_up_stop_delay)
1058 # Final boolean decision.
1059 ramp_up_needed = (bigger_delay >= self.state_timeout)
1062 u"State may time out during next real trial, "
1063 u"inserting a ramp-up trial."
1065 self.ramp_up_start = time.monotonic()
1066 self._send_traffic_on_tg_internal(
1067 duration=self.ramp_up_duration,
1068 rate=self.ramp_up_rate,
1069 async_call=async_call,
1071 self.ramp_up_stop = time.monotonic()
1072 logger.debug(u"Ramp-up done.")
1075 u"State will probably not time out during next real trial, "
1076 u"no ramp-up trial needed just yet."
1080 trial_start = time.monotonic()
1081 result = self._send_traffic_on_tg_internal(
1084 async_call=async_call,
1086 trial_end = time.monotonic()
1087 if self.ramp_up_rate:
1088 # Optimization: No loss acts as a good ramp-up, if it was complete.
1089 if complete and result is not None and result.loss_count == 0:
1090 logger.debug(u"Good trial acts as a ramp-up")
1091 self.ramp_up_start = trial_start
1092 self.ramp_up_stop = trial_end
1094 logger.debug(u"Loss or incomplete, does not act as a ramp-up.")
1097 def no_traffic_loss_occurred(self):
1098 """Fail if loss occurred in traffic run.
1101 :raises Exception: If loss occured.
1103 if self._loss is None:
1104 raise RuntimeError(u"The traffic generation has not been issued")
1105 if self._loss != u"0":
1106 raise RuntimeError(f"Traffic loss occurred: {self._loss}")
1108 def fail_if_no_traffic_forwarded(self):
1109 """Fail if no traffic forwarded.
1111 TODO: Check number of passed transactions instead.
1114 :raises Exception: If no traffic forwarded.
1116 if self._received is None:
1117 raise RuntimeError(u"The traffic generation has not been issued")
1118 if self._received == 0:
1119 raise RuntimeError(u"No traffic forwarded")
1121 def partial_traffic_loss_accepted(
1122 self, loss_acceptance, loss_acceptance_type):
1123 """Fail if loss is higher then accepted in traffic run.
1125 :param loss_acceptance: Permitted drop ratio or frames count.
1126 :param loss_acceptance_type: Type of permitted loss.
1127 :type loss_acceptance: float
1128 :type loss_acceptance_type: LossAcceptanceType
1130 :raises Exception: If loss is above acceptance criteria.
1132 if self._loss is None:
1133 raise Exception(u"The traffic generation has not been issued")
1135 if loss_acceptance_type == u"percentage":
1136 loss = (float(self._loss) / float(self._sent)) * 100
1137 elif loss_acceptance_type == u"frames":
1138 loss = float(self._loss)
1140 raise Exception(u"Loss acceptance type not supported")
1142 if loss > float(loss_acceptance):
1144 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
1147 def _parse_traffic_results(self, stdout):
1148 """Parse stdout of scripts into fields of self.
1150 Block of code to reuse, by sync start, or stop after async.
1152 :param stdout: Text containing the standard output.
1155 subtype = check_subtype(self._node)
1156 if subtype == NodeSubTypeTG.TREX:
1157 # Last line from console output
1158 line = stdout.splitlines()[-1]
1159 results = line.split(u";")
1160 if results[-1] in (u" ", u""):
1162 self._result = dict()
1163 for result in results:
1164 key, value = result.split(u"=", maxsplit=1)
1165 self._result[key.strip()] = value
1166 logger.info(f"TrafficGen results:\n{self._result}")
1167 self._received = int(self._result.get(u"total_received"), 0)
1168 self._sent = int(self._result.get(u"total_sent", 0))
1169 self._loss = int(self._result.get(u"frame_loss", 0))
1170 self._approximated_duration = \
1171 self._result.get(u"approximated_duration", 0.0)
1172 if u"manual" not in str(self._approximated_duration):
1173 self._approximated_duration = float(self._approximated_duration)
1174 self._latency = list()
1175 self._latency.append(self._result.get(u"latency_stream_0(usec)"))
1176 self._latency.append(self._result.get(u"latency_stream_1(usec)"))
1177 if self._mode == TrexMode.ASTF:
1178 self._l7_data = dict()
1179 self._l7_data[u"client"] = dict()
1180 self._l7_data[u"client"][u"sent"] = \
1181 int(self._result.get(u"client_sent", 0))
1182 self._l7_data[u"client"][u"received"] = \
1183 int(self._result.get(u"client_received", 0))
1184 self._l7_data[u"client"][u"active_flows"] = \
1185 int(self._result.get(u"client_active_flows", 0))
1186 self._l7_data[u"client"][u"established_flows"] = \
1187 int(self._result.get(u"client_established_flows", 0))
1188 self._l7_data[u"client"][u"traffic_duration"] = \
1189 float(self._result.get(u"client_traffic_duration", 0.0))
1190 self._l7_data[u"client"][u"err_rx_throttled"] = \
1191 int(self._result.get(u"client_err_rx_throttled", 0))
1192 self._l7_data[u"client"][u"err_c_nf_throttled"] = \
1193 int(self._result.get(u"client_err_nf_throttled", 0))
1194 self._l7_data[u"client"][u"err_flow_overflow"] = \
1195 int(self._result.get(u"client_err_flow_overflow", 0))
1196 self._l7_data[u"server"] = dict()
1197 self._l7_data[u"server"][u"active_flows"] = \
1198 int(self._result.get(u"server_active_flows", 0))
1199 self._l7_data[u"server"][u"established_flows"] = \
1200 int(self._result.get(u"server_established_flows", 0))
1201 self._l7_data[u"server"][u"traffic_duration"] = \
1202 float(self._result.get(u"server_traffic_duration", 0.0))
1203 self._l7_data[u"server"][u"err_rx_throttled"] = \
1204 int(self._result.get(u"client_err_rx_throttled", 0))
1205 if u"udp" in self.traffic_profile:
1206 self._l7_data[u"client"][u"udp"] = dict()
1207 self._l7_data[u"client"][u"udp"][u"connects"] = \
1208 int(self._result.get(u"client_udp_connects", 0))
1209 self._l7_data[u"client"][u"udp"][u"closed_flows"] = \
1210 int(self._result.get(u"client_udp_closed", 0))
1211 self._l7_data[u"client"][u"udp"][u"tx_bytes"] = \
1212 int(self._result.get(u"client_udp_tx_bytes", 0))
1213 self._l7_data[u"client"][u"udp"][u"rx_bytes"] = \
1214 int(self._result.get(u"client_udp_rx_bytes", 0))
1215 self._l7_data[u"client"][u"udp"][u"tx_packets"] = \
1216 int(self._result.get(u"client_udp_tx_packets", 0))
1217 self._l7_data[u"client"][u"udp"][u"rx_packets"] = \
1218 int(self._result.get(u"client_udp_rx_packets", 0))
1219 self._l7_data[u"client"][u"udp"][u"keep_drops"] = \
1220 int(self._result.get(u"client_udp_keep_drops", 0))
1221 self._l7_data[u"client"][u"udp"][u"err_cwf"] = \
1222 int(self._result.get(u"client_err_cwf", 0))
1223 self._l7_data[u"server"][u"udp"] = dict()
1224 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = \
1225 int(self._result.get(u"server_udp_accepts", 0))
1226 self._l7_data[u"server"][u"udp"][u"closed_flows"] = \
1227 int(self._result.get(u"server_udp_closed", 0))
1228 self._l7_data[u"server"][u"udp"][u"tx_bytes"] = \
1229 int(self._result.get(u"server_udp_tx_bytes", 0))
1230 self._l7_data[u"server"][u"udp"][u"rx_bytes"] = \
1231 int(self._result.get(u"server_udp_rx_bytes", 0))
1232 self._l7_data[u"server"][u"udp"][u"tx_packets"] = \
1233 int(self._result.get(u"server_udp_tx_packets", 0))
1234 self._l7_data[u"server"][u"udp"][u"rx_packets"] = \
1235 int(self._result.get(u"server_udp_rx_packets", 0))
1236 elif u"tcp" in self.traffic_profile:
1237 self._l7_data[u"client"][u"tcp"] = dict()
1238 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = \
1239 int(self._result.get(u"client_tcp_connect_inits", 0))
1240 self._l7_data[u"client"][u"tcp"][u"connects"] = \
1241 int(self._result.get(u"client_tcp_connects", 0))
1242 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = \
1243 int(self._result.get(u"client_tcp_closed", 0))
1244 self._l7_data[u"client"][u"tcp"][u"connattempt"] = \
1245 int(self._result.get(u"client_tcp_connattempt", 0))
1246 self._l7_data[u"client"][u"tcp"][u"tx_bytes"] = \
1247 int(self._result.get(u"client_tcp_tx_bytes", 0))
1248 self._l7_data[u"client"][u"tcp"][u"rx_bytes"] = \
1249 int(self._result.get(u"client_tcp_rx_bytes", 0))
1250 self._l7_data[u"server"][u"tcp"] = dict()
1251 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = \
1252 int(self._result.get(u"server_tcp_accepts", 0))
1253 self._l7_data[u"server"][u"tcp"][u"connects"] = \
1254 int(self._result.get(u"server_tcp_connects", 0))
1255 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = \
1256 int(self._result.get(u"server_tcp_closed", 0))
1257 self._l7_data[u"server"][u"tcp"][u"tx_bytes"] = \
1258 int(self._result.get(u"server_tcp_tx_bytes", 0))
1259 self._l7_data[u"server"][u"tcp"][u"rx_bytes"] = \
1260 int(self._result.get(u"server_tcp_rx_bytes", 0))
1262 def _get_measurement_result(self):
1263 """Return the result of last measurement as ReceiveRateMeasurement.
1265 Separate function, as measurements can end either by time
1266 or by explicit call, this is the common block at the end.
1268 The target_tr field of ReceiveRateMeasurement is in
1269 transactions per second. Transmit count and loss count units
1270 depend on the transaction type. Usually they are in transactions
1271 per second, or aggregated packets per second.
1273 TODO: Fail on running or already reported measurement.
1275 :returns: Structure containing the result of the measurement.
1276 :rtype: ReceiveRateMeasurement
1279 # Client duration seems to include a setup period
1280 # where TRex does not send any packets yet.
1281 # Server duration does not include it.
1282 server_data = self._l7_data[u"server"]
1283 approximated_duration = float(server_data[u"traffic_duration"])
1284 except (KeyError, AttributeError, ValueError, TypeError):
1285 approximated_duration = None
1287 if not approximated_duration:
1288 approximated_duration = float(self._approximated_duration)
1289 except ValueError: # "manual"
1290 approximated_duration = None
1291 if not approximated_duration:
1292 if self._duration and self._duration > 0:
1293 # Known recomputed or target duration.
1294 approximated_duration = self._duration
1296 # It was an explicit stop.
1297 if not self._stop_time:
1298 raise RuntimeError(u"Unable to determine duration.")
1299 approximated_duration = self._stop_time - self._start_time
1300 target_duration = self._target_duration
1301 if not target_duration:
1302 target_duration = approximated_duration
1303 transmit_rate = self._rate
1305 if self.transaction_type == u"packet":
1306 partial_attempt_count = self._sent
1307 packet_rate = transmit_rate * self.ppta
1308 # We have a float. TRex way of rounding it is not obvious.
1309 # The biggest source of mismatch is Inter Stream Gap.
1310 # So the code tolerates 10 usec of missing packets.
1311 expected_attempt_count = (target_duration - 1e-5) * packet_rate
1312 expected_attempt_count = math.ceil(expected_attempt_count)
1313 # TRex can send more.
1314 expected_attempt_count = max(expected_attempt_count, self._sent)
1315 unsent = expected_attempt_count - self._sent
1316 pass_count = self._received
1317 fail_count = expected_attempt_count - pass_count
1318 elif self.transaction_type == u"udp_cps":
1319 if not self.transaction_scale:
1320 raise RuntimeError(u"Add support for no-limit udp_cps.")
1321 partial_attempt_count = self._l7_data[u"client"][u"sent"]
1322 # We do not care whether TG is slow, it should have attempted all.
1323 expected_attempt_count = self.transaction_scale
1324 unsent = expected_attempt_count - partial_attempt_count
1325 pass_count = self._l7_data[u"client"][u"received"]
1326 fail_count = expected_attempt_count - pass_count
1327 elif self.transaction_type == u"tcp_cps":
1328 if not self.transaction_scale:
1329 raise RuntimeError(u"Add support for no-limit tcp_cps.")
1330 ctca = self._l7_data[u"client"][u"tcp"][u"connattempt"]
1331 partial_attempt_count = ctca
1332 # We do not care whether TG is slow, it should have attempted all.
1333 expected_attempt_count = self.transaction_scale
1334 unsent = expected_attempt_count - partial_attempt_count
1335 # From TCP point of view, server/connects counts full connections,
1336 # but we are testing NAT session so client/connects counts that
1337 # (half connections from TCP point of view).
1338 pass_count = self._l7_data[u"client"][u"tcp"][u"connects"]
1339 fail_count = expected_attempt_count - pass_count
1340 elif self.transaction_type == u"udp_pps":
1341 if not self.transaction_scale:
1342 raise RuntimeError(u"Add support for no-limit udp_pps.")
1343 partial_attempt_count = self._sent
1344 expected_attempt_count = self.transaction_scale * self.ppta
1345 unsent = expected_attempt_count - self._sent
1346 fail_count = self._loss + unsent
1347 elif self.transaction_type == u"tcp_pps":
1348 if not self.transaction_scale:
1349 raise RuntimeError(u"Add support for no-limit tcp_pps.")
1350 partial_attempt_count = self._sent
1351 expected_attempt_count = self.transaction_scale * self.ppta
1352 # One loss-like scenario happens when TRex receives all packets
1353 # on L2 level, but is not fast enough to process them all
1354 # at L7 level, which leads to retransmissions.
1355 # Those manifest as opackets larger than expected.
1356 # A simple workaround is to add absolute difference.
1357 # Probability of retransmissions exactly cancelling
1358 # packets unsent due to duration stretching is quite low.
1359 unsent = abs(expected_attempt_count - self._sent)
1360 fail_count = self._loss + unsent
1362 raise RuntimeError(f"Unknown parsing {self.transaction_type!r}")
1363 if unsent and isinstance(self._approximated_duration, float):
1364 # Do not report unsent for "manual".
1365 logger.debug(f"Unsent packets/transactions: {unsent}")
1366 if fail_count < 0 and not self.negative_loss:
1368 measurement = ReceiveRateMeasurement(
1369 duration=target_duration,
1370 target_tr=transmit_rate,
1371 transmit_count=expected_attempt_count,
1372 loss_count=fail_count,
1373 approximated_duration=approximated_duration,
1374 partial_transmit_count=partial_attempt_count,
1376 measurement.latency = self.get_latency_int()
1379 def measure(self, duration, transmit_rate):
1380 """Run trial measurement, parse and return results.
1382 The input rate is for transactions. Stateles bidirectional traffic
1383 is understood as sequence of (asynchronous) transactions,
1386 The result units depend on test type, generally
1387 the count either transactions or packets (aggregated over directions).
1389 Optionally, this method sleeps if measurement finished before
1390 the time specified as duration.
1392 :param duration: Trial duration [s].
1393 :param transmit_rate: Target rate in transactions per second.
1394 :type duration: float
1395 :type transmit_rate: float
1396 :returns: Structure containing the result of the measurement.
1397 :rtype: ReceiveRateMeasurement
1398 :raises RuntimeError: If TG is not set or if node is not TG
1399 or if subtype is not specified.
1400 :raises NotImplementedError: If TG is not supported.
1402 duration = float(duration)
1403 time_start = time.monotonic()
1404 time_stop = time_start + duration
1407 result = self._send_traffic_on_tg_with_ramp_up(
1412 logger.debug(f"trial measurement result: {result!r}")
1413 # In PLRsearch, computation needs the specified time to complete.
1414 if self.sleep_till_duration:
1415 sleeptime = time_stop - time.monotonic()
1417 # TODO: Sometimes we have time to do additional trials here,
1418 # adapt PLRsearch to accept all the results.
1419 time.sleep(sleeptime)
1422 def set_rate_provider_defaults(
1428 traffic_directions=2,
1429 transaction_duration=0.0,
1430 transaction_scale=0,
1431 transaction_type=u"packet",
1434 sleep_till_duration=False,
1437 ramp_up_duration=None,
1438 state_timeout=240.0,
1440 """Store values accessed by measure().
1442 :param frame_size: Frame size identifier or value [B].
1443 :param traffic_profile: Module name as a traffic profile identifier.
1444 See GPL/traffic_profiles/trex for implemented modules.
1445 :param ppta: Packets per transaction, aggregated over directions.
1446 Needed for udp_pps which does not have a good transaction counter,
1447 so we need to compute expected number of packets.
1449 :param resetter: Callable to reset DUT state for repeated trials.
1450 :param traffic_directions: Traffic from packet counting point of view
1451 is bi- (2) or uni- (1) directional.
1453 :param transaction_duration: Total expected time to close transaction.
1454 :param transaction_scale: Number of transactions to perform.
1455 0 (default) means unlimited.
1456 :param transaction_type: An identifier specifying which counters
1457 and formulas to use when computing attempted and failed
1458 transactions. Default: "packet".
1459 TODO: Does this also specify parsing for the measured duration?
1460 :param duration_limit: Zero or maximum limit for computed (or given)
1462 :param negative_loss: If false, negative loss is reported as zero loss.
1463 :param sleep_till_duration: If true and measurement returned faster,
1464 sleep until it matches duration. Needed for PLRsearch.
1465 :param use_latency: Whether to measure latency during the trial.
1467 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1468 :param ramp_up_duration: Duration of ramp-up trials [s].
1469 :param state_timeout: Time of life of DUT state [s].
1470 :type frame_size: str or int
1471 :type traffic_profile: str
1473 :type resetter: Optional[Callable[[], None]]
1474 :type traffic_directions: int
1475 :type transaction_duration: float
1476 :type transaction_scale: int
1477 :type transaction_type: str
1478 :type duration_limit: float
1479 :type negative_loss: bool
1480 :type sleep_till_duration: bool
1481 :type use_latency: bool
1482 :type ramp_up_rate: float
1483 :type ramp_up_duration: float
1484 :type state_timeout: float
1486 self.frame_size = frame_size
1487 self.traffic_profile = str(traffic_profile)
1488 self.resetter = resetter
1490 self.traffic_directions = int(traffic_directions)
1491 self.transaction_duration = float(transaction_duration)
1492 self.transaction_scale = int(transaction_scale)
1493 self.transaction_type = str(transaction_type)
1494 self.duration_limit = float(duration_limit)
1495 self.negative_loss = bool(negative_loss)
1496 self.sleep_till_duration = bool(sleep_till_duration)
1497 self.use_latency = bool(use_latency)
1498 self.ramp_up_rate = float(ramp_up_rate)
1499 self.ramp_up_duration = float(ramp_up_duration)
1500 self.state_timeout = float(state_timeout)
1503 class OptimizedSearch:
1504 """Class to be imported as Robot Library, containing search keywords.
1506 Aside of setting up measurer and forwarding arguments,
1507 the main business is to translate min/max rate from unidir to aggregated.
1511 def perform_optimized_ndrpdr_search(
1514 minimum_transmit_rate,
1515 maximum_transmit_rate,
1516 packet_loss_ratio=0.005,
1517 final_relative_width=0.005,
1518 final_trial_duration=30.0,
1519 initial_trial_duration=1.0,
1520 number_of_intermediate_phases=2,
1524 traffic_directions=2,
1525 transaction_duration=0.0,
1526 transaction_scale=0,
1527 transaction_type=u"packet",
1530 ramp_up_duration=None,
1531 state_timeout=240.0,
1532 expansion_coefficient=4.0,
1534 """Setup initialized TG, perform optimized search, return intervals.
1536 If transaction_scale is nonzero, all init and non-init trial durations
1537 are set to 1.0 (as they do not affect the real trial duration)
1538 and zero intermediate phases are used.
1539 This way no re-measurement happens.
1540 Warmup has to be handled via resetter or ramp-up mechanisms.
1542 :param frame_size: Frame size identifier or value [B].
1543 :param traffic_profile: Module name as a traffic profile identifier.
1544 See GPL/traffic_profiles/trex for implemented modules.
1545 :param minimum_transmit_rate: Minimal load in transactions per second.
1546 :param maximum_transmit_rate: Maximal load in transactions per second.
1547 :param packet_loss_ratio: Ratio of packets lost, for PDR [1].
1548 :param final_relative_width: Final lower bound transmit rate
1549 cannot be more distant that this multiple of upper bound [1].
1550 :param final_trial_duration: Trial duration for the final phase [s].
1551 :param initial_trial_duration: Trial duration for the initial phase
1552 and also for the first intermediate phase [s].
1553 :param number_of_intermediate_phases: Number of intermediate phases
1554 to perform before the final phase [1].
1555 :param timeout: The search will fail itself when not finished
1556 before this overall time [s].
1557 :param ppta: Packets per transaction, aggregated over directions.
1558 Needed for udp_pps which does not have a good transaction counter,
1559 so we need to compute expected number of packets.
1561 :param resetter: Callable to reset DUT state for repeated trials.
1562 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1564 :param transaction_duration: Total expected time to close transaction.
1565 :param transaction_scale: Number of transactions to perform.
1566 0 (default) means unlimited.
1567 :param transaction_type: An identifier specifying which counters
1568 and formulas to use when computing attempted and failed
1569 transactions. Default: "packet".
1570 :param use_latency: Whether to measure latency during the trial.
1572 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1573 :param ramp_up_duration: Duration of ramp-up trials [s].
1574 :param state_timeout: Time of life of DUT state [s].
1575 :param expansion_coefficient: In external search multiply width by this.
1576 :type frame_size: str or int
1577 :type traffic_profile: str
1578 :type minimum_transmit_rate: float
1579 :type maximum_transmit_rate: float
1580 :type packet_loss_ratio: float
1581 :type final_relative_width: float
1582 :type final_trial_duration: float
1583 :type initial_trial_duration: float
1584 :type number_of_intermediate_phases: int
1585 :type timeout: float
1587 :type resetter: Optional[Callable[[], None]]
1588 :type traffic_directions: int
1589 :type transaction_duration: float
1590 :type transaction_scale: int
1591 :type transaction_type: str
1592 :type use_latency: bool
1593 :type ramp_up_rate: float
1594 :type ramp_up_duration: float
1595 :type state_timeout: float
1596 :type expansion_coefficient: float
1597 :returns: Structure containing narrowed down NDR and PDR intervals
1598 and their measurements.
1599 :rtype: List[Receiverateinterval]
1600 :raises RuntimeError: If total duration is larger than timeout.
1602 # we need instance of TrafficGenerator instantiated by Robot Framework
1603 # to be able to use trex_stl-*()
1604 tg_instance = BuiltIn().get_library_instance(
1605 u"resources.libraries.python.TrafficGenerator"
1607 # Overrides for fixed transaction amount.
1608 # TODO: Move to robot code? We have two call sites, so this saves space,
1609 # even though this is surprising for log readers.
1610 if transaction_scale:
1611 initial_trial_duration = 1.0
1612 final_trial_duration = 1.0
1613 number_of_intermediate_phases = 0
1614 timeout += transaction_scale * 3e-4
1615 tg_instance.set_rate_provider_defaults(
1616 frame_size=frame_size,
1617 traffic_profile=traffic_profile,
1618 sleep_till_duration=False,
1621 traffic_directions=traffic_directions,
1622 transaction_duration=transaction_duration,
1623 transaction_scale=transaction_scale,
1624 transaction_type=transaction_type,
1625 use_latency=use_latency,
1626 ramp_up_rate=ramp_up_rate,
1627 ramp_up_duration=ramp_up_duration,
1628 state_timeout=state_timeout,
1630 algorithm = MultipleLossRatioSearch(
1631 measurer=tg_instance,
1632 final_trial_duration=final_trial_duration,
1633 final_relative_width=final_relative_width,
1634 number_of_intermediate_phases=number_of_intermediate_phases,
1635 initial_trial_duration=initial_trial_duration,
1638 expansion_coefficient=expansion_coefficient,
1640 if packet_loss_ratio:
1641 packet_loss_ratios = [0.0, packet_loss_ratio]
1643 # Happens in reconf tests.
1644 packet_loss_ratios = [packet_loss_ratio]
1645 results = algorithm.narrow_down_intervals(
1646 min_rate=minimum_transmit_rate,
1647 max_rate=maximum_transmit_rate,
1648 packet_loss_ratios=packet_loss_ratios,
1653 def perform_soak_search(
1656 minimum_transmit_rate,
1657 maximum_transmit_rate,
1664 trace_enabled=False,
1665 traffic_directions=2,
1666 transaction_duration=0.0,
1667 transaction_scale=0,
1668 transaction_type=u"packet",
1671 ramp_up_duration=None,
1672 state_timeout=240.0,
1674 """Setup initialized TG, perform soak search, return avg and stdev.
1676 :param frame_size: Frame size identifier or value [B].
1677 :param traffic_profile: Module name as a traffic profile identifier.
1678 See GPL/traffic_profiles/trex for implemented modules.
1679 :param minimum_transmit_rate: Minimal load in transactions per second.
1680 :param maximum_transmit_rate: Maximal load in transactions per second.
1681 :param plr_target: Ratio of packets lost to achieve [1].
1682 :param tdpt: Trial duration per trial.
1683 The algorithm linearly increases trial duration with trial number,
1684 this is the increment between succesive trials, in seconds.
1685 :param initial_count: Offset to apply before the first trial.
1686 For example initial_count=50 makes first trial to be 51*tdpt long.
1687 This is needed because initial "search" phase of integrator
1688 takes significant time even without any trial results.
1689 :param timeout: The search will stop after this overall time [s].
1690 :param ppta: Packets per transaction, aggregated over directions.
1691 Needed for udp_pps which does not have a good transaction counter,
1692 so we need to compute expected number of packets.
1694 :param resetter: Callable to reset DUT state for repeated trials.
1695 :param trace_enabled: True if trace enabled else False.
1696 This is very verbose tracing on numeric computations,
1697 do not use in production.
1699 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1701 :param transaction_duration: Total expected time to close transaction.
1702 :param transaction_scale: Number of transactions to perform.
1703 0 (default) means unlimited.
1704 :param transaction_type: An identifier specifying which counters
1705 and formulas to use when computing attempted and failed
1706 transactions. Default: "packet".
1707 :param use_latency: Whether to measure latency during the trial.
1709 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1710 :param ramp_up_duration: Duration of ramp-up trials [s].
1711 :param state_timeout: Time of life of DUT state [s].
1712 :type frame_size: str or int
1713 :type traffic_profile: str
1714 :type minimum_transmit_rate: float
1715 :type maximum_transmit_rate: float
1716 :type plr_target: float
1717 :type initial_count: int
1718 :type timeout: float
1720 :type resetter: Optional[Callable[[], None]]
1721 :type trace_enabled: bool
1722 :type traffic_directions: int
1723 :type transaction_duration: float
1724 :type transaction_scale: int
1725 :type transaction_type: str
1726 :type use_latency: bool
1727 :type ramp_up_rate: float
1728 :type ramp_up_duration: float
1729 :type state_timeout: float
1730 :returns: Average and stdev of estimated aggregated rate giving PLR.
1731 :rtype: 2-tuple of float
1733 tg_instance = BuiltIn().get_library_instance(
1734 u"resources.libraries.python.TrafficGenerator"
1736 # Overrides for fixed transaction amount.
1737 # TODO: Move to robot code? We have a single call site
1738 # but MLRsearch has two and we want the two to be used similarly.
1739 if transaction_scale:
1740 # TODO: What is a good value for max scale?
1741 # TODO: Scale the timeout with transaction scale.
1743 tg_instance.set_rate_provider_defaults(
1744 frame_size=frame_size,
1745 traffic_profile=traffic_profile,
1746 negative_loss=False,
1747 sleep_till_duration=True,
1750 traffic_directions=traffic_directions,
1751 transaction_duration=transaction_duration,
1752 transaction_scale=transaction_scale,
1753 transaction_type=transaction_type,
1754 use_latency=use_latency,
1755 ramp_up_rate=ramp_up_rate,
1756 ramp_up_duration=ramp_up_duration,
1757 state_timeout=state_timeout,
1759 algorithm = PLRsearch(
1760 measurer=tg_instance,
1761 trial_duration_per_trial=tdpt,
1762 packet_loss_ratio_target=plr_target,
1763 trial_number_offset=initial_count,
1765 trace_enabled=trace_enabled,
1767 result = algorithm.search(
1768 min_rate=minimum_transmit_rate,
1769 max_rate=maximum_transmit_rate,