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!"
428 for port in tg_node[u"interfaces"].values():
429 if u'Mellanox' not in port.get(u'model'):
430 ports += f" {port.get(u'pci_address')}"
432 cmd = f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && " \
433 f"./dpdk_nic_bind.py -u {ports} || true\""
435 tg_node, cmd, sudo=True,
436 message=u"Unbind PCI ports from driver failed!"
440 cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
441 trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
443 trex_cmd.add(u"--prefix $(hostname)")
444 trex_cmd.add(u"--hdrh")
445 trex_cmd.add(u"--no-scapy-server")
446 trex_cmd.add_if(u"--astf", osi_layer == u"L7")
447 # OptionString does not create double space if extra is empty.
448 trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
449 inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
450 cmd = f"sh -c \"{inner_command}\" > /dev/null"
452 exec_cmd_no_error(tg_node, cmd, sudo=True)
454 cmd = u"sh -c \"cat /tmp/trex.log\""
456 tg_node, cmd, sudo=True,
457 message=u"Get TRex logs failed!"
459 raise RuntimeError(u"Start TRex failed!")
461 # Test T-Rex API responsiveness.
462 cmd = f"python3 {Constants.REMOTE_FW_DIR}/GPL/tools/trex/"
463 if osi_layer in (u"L2", u"L3"):
464 cmd += u"trex_stl_assert.py"
465 elif osi_layer == u"L7":
466 cmd += u"trex_astf_assert.py"
468 raise ValueError(u"Unknown OSI layer!")
471 tg_node, cmd, sudo=True,
472 message=u"T-Rex API is not responding!", retries=20
477 # After max retries TRex is still not responding to API critical
479 exec_cmd(tg_node, u"cat /tmp/trex.log", sudo=True)
480 raise RuntimeError(u"Start T-Rex failed after multiple retries!")
483 def is_trex_running(node):
484 """Check if T-Rex is running using pidof.
486 :param node: Traffic generator node.
488 :returns: True if T-Rex is running otherwise False.
491 ret, _, _ = exec_cmd(node, u"pgrep t-rex", sudo=True)
492 return bool(int(ret) == 0)
495 def teardown_traffic_generator(node):
498 :param node: Traffic generator node.
501 :raises RuntimeError: If node type is not a TG,
502 or if T-Rex teardown fails.
504 subtype = check_subtype(node)
505 if subtype == NodeSubTypeTG.TREX:
509 u"\"if pgrep t-rex; then sudo pkill t-rex && sleep 3; fi\"",
511 message=u"T-Rex kill failed!"
514 def trex_astf_stop_remote_exec(self, node):
515 """Execute T-Rex ASTF script on remote node over ssh to stop running
518 Internal state is updated with measurement results.
520 :param node: T-Rex generator node.
522 :raises RuntimeError: If stop traffic script fails.
524 command_line = OptionString().add(u"python3")
525 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
526 command_line.add(f"'{dirname}/trex_astf_stop.py'")
527 command_line.change_prefix(u"--")
528 for index, value in enumerate(self._xstats):
529 if value is not None:
530 value = value.replace(u"'", u"\"")
531 command_line.add_equals(f"xstat{index}", f"'{value}'")
532 stdout, _ = exec_cmd_no_error(
534 message=u"T-Rex ASTF runtime error!"
536 self._parse_traffic_results(stdout)
538 def trex_stl_stop_remote_exec(self, node):
539 """Execute T-Rex STL script on remote node over ssh to stop running
542 Internal state is updated with measurement results.
544 :param node: T-Rex generator node.
546 :raises RuntimeError: If stop traffic script fails.
548 command_line = OptionString().add(u"python3")
549 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
550 command_line.add(f"'{dirname}/trex_stl_stop.py'")
551 command_line.change_prefix(u"--")
552 for index, value in enumerate(self._xstats):
553 if value is not None:
554 value = value.replace(u"'", u"\"")
555 command_line.add_equals(f"xstat{index}", f"'{value}'")
556 stdout, _ = exec_cmd_no_error(
558 message=u"T-Rex STL runtime error!"
560 self._parse_traffic_results(stdout)
562 def stop_traffic_on_tg(self):
563 """Stop all traffic on TG.
565 :returns: Structure containing the result of the measurement.
566 :rtype: ReceiveRateMeasurement
567 :raises ValueError: If TG traffic profile is not supported.
569 subtype = check_subtype(self._node)
570 if subtype != NodeSubTypeTG.TREX:
571 raise ValueError(f"Unsupported TG subtype: {subtype!r}")
572 if u"trex-astf" in self.traffic_profile:
573 self.trex_astf_stop_remote_exec(self._node)
574 elif u"trex-stl" in self.traffic_profile:
575 self.trex_stl_stop_remote_exec(self._node)
577 raise ValueError(u"Unsupported T-Rex traffic profile!")
578 self._stop_time = time.monotonic()
580 return self._get_measurement_result()
582 def _compute_duration(self, duration, multiplier):
583 """Compute duration for profile driver.
585 The final result is influenced by transaction scale and duration limit.
586 It is assumed a higher level function has already set those to self.
587 The duration argument is the target value from search point of view,
588 before the overrides are applied here.
590 Minus one (signalling async traffic start) is kept.
592 Completeness flag is also included. Duration limited or async trials
593 are not considered complete for ramp-up purposes.
595 :param duration: Time expressed in seconds for how long to send traffic.
596 :param multiplier: Traffic rate in transactions per second.
597 :type duration: float
598 :type multiplier: float
599 :returns: New duration and whether it was a complete ramp-up candidate.
604 return duration, False
605 computed_duration = duration
606 if self.transaction_scale:
607 computed_duration = self.transaction_scale / multiplier
608 # Log the computed duration,
609 # so we can compare with what telemetry suggests
610 # the real duration was.
611 logger.debug(f"Expected duration {computed_duration}")
612 if not self.duration_limit:
613 return computed_duration, True
614 limited_duration = min(computed_duration, self.duration_limit)
615 return limited_duration, (limited_duration == computed_duration)
617 def trex_astf_start_remote_exec(
618 self, duration, multiplier, async_call=False):
619 """Execute T-Rex ASTF script on remote node over ssh to start running
622 In sync mode, measurement results are stored internally.
623 In async mode, initial data including xstats are stored internally.
625 This method contains the logic to compute duration as maximum time
626 if transaction_scale is nonzero.
627 The transaction_scale argument defines (limits) how many transactions
628 will be started in total. As that amount of transaction can take
629 considerable time (sometimes due to explicit delays in the profile),
630 the real time a trial needs to finish is computed here. For now,
631 in that case the duration argument is ignored, assuming it comes
632 from ASTF-unaware search algorithm. The overall time a single
633 transaction needs is given in parameter transaction_duration,
634 it includes both explicit delays and implicit time it takes
635 to transfer data (or whatever the transaction does).
637 Currently it is observed TRex does not start the ASTF traffic
638 immediately, an ad-hoc constant is added to the computed duration
639 to compensate for that.
641 If transaction_scale is zero, duration is not recomputed.
642 It is assumed the subsequent result parsing gets the real duration
643 if the traffic stops sooner for any reason.
645 Currently, it is assumed traffic profile defines a single transaction.
646 To avoid heavy logic here, the input rate is expected to be in
647 transactions per second, as that directly translates to TRex multiplier,
648 (assuming the profile does not override the default cps value of one).
650 :param duration: Time expressed in seconds for how long to send traffic.
651 :param multiplier: Traffic rate in transactions per second.
652 :param async_call: If enabled then don't wait for all incoming traffic.
653 :type duration: float
654 :type multiplier: int
655 :type async_call: bool
656 :raises RuntimeError: In case of T-Rex driver issue.
658 self.check_mode(TrexMode.ASTF)
659 p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
660 if not isinstance(duration, (float, int)):
661 duration = float(duration)
663 # TODO: Refactor the code so duration is computed only once,
664 # and both the initial and the computed durations are logged.
665 computed_duration, _ = self._compute_duration(duration, multiplier)
667 command_line = OptionString().add(u"python3")
668 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
669 command_line.add(f"'{dirname}/trex_astf_profile.py'")
670 command_line.change_prefix(u"--")
671 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
672 command_line.add_with_value(
673 u"profile", f"'{dirname}/{self.traffic_profile}.py'"
675 command_line.add_with_value(u"duration", f"{computed_duration!r}")
676 command_line.add_with_value(u"frame_size", self.frame_size)
677 command_line.add_with_value(
678 u"n_data_frames", Constants.ASTF_N_DATA_FRAMES
680 command_line.add_with_value(u"multiplier", multiplier)
681 command_line.add_with_value(u"port_0", p_0)
682 command_line.add_with_value(u"port_1", p_1)
683 command_line.add_with_value(
684 u"traffic_directions", self.traffic_directions
686 command_line.add_if(u"async_start", async_call)
687 command_line.add_if(u"latency", self.use_latency)
688 command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
689 command_line.add_with_value(
690 u"delay", Constants.PERF_TRIAL_ASTF_DELAY
693 self._start_time = time.monotonic()
694 self._rate = multiplier
695 stdout, _ = exec_cmd_no_error(
696 self._node, command_line, timeout=computed_duration + 10.0,
697 message=u"T-Rex ASTF runtime error!"
702 self._target_duration = None
703 self._duration = None
704 self._received = None
708 xstats = [None, None]
709 self._l7_data = dict()
710 self._l7_data[u"client"] = dict()
711 self._l7_data[u"client"][u"active_flows"] = None
712 self._l7_data[u"client"][u"established_flows"] = None
713 self._l7_data[u"client"][u"traffic_duration"] = None
714 self._l7_data[u"server"] = dict()
715 self._l7_data[u"server"][u"active_flows"] = None
716 self._l7_data[u"server"][u"established_flows"] = None
717 self._l7_data[u"server"][u"traffic_duration"] = None
718 if u"udp" in self.traffic_profile:
719 self._l7_data[u"client"][u"udp"] = dict()
720 self._l7_data[u"client"][u"udp"][u"connects"] = None
721 self._l7_data[u"client"][u"udp"][u"closed_flows"] = None
722 self._l7_data[u"client"][u"udp"][u"err_cwf"] = None
723 self._l7_data[u"server"][u"udp"] = dict()
724 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = None
725 self._l7_data[u"server"][u"udp"][u"closed_flows"] = None
726 elif u"tcp" in self.traffic_profile:
727 self._l7_data[u"client"][u"tcp"] = dict()
728 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = None
729 self._l7_data[u"client"][u"tcp"][u"connects"] = None
730 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = None
731 self._l7_data[u"client"][u"tcp"][u"connattempt"] = None
732 self._l7_data[u"server"][u"tcp"] = dict()
733 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = None
734 self._l7_data[u"server"][u"tcp"][u"connects"] = None
735 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = None
737 logger.warn(u"Unsupported T-Rex ASTF traffic profile!")
739 for line in stdout.splitlines():
740 if f"Xstats snapshot {index}: " in line:
741 xstats[index] = line[19:]
745 self._xstats = tuple(xstats)
747 self._target_duration = duration
748 self._duration = computed_duration
749 self._parse_traffic_results(stdout)
751 def trex_stl_start_remote_exec(self, duration, rate, async_call=False):
752 """Execute T-Rex STL script on remote node over ssh to start running
755 In sync mode, measurement results are stored internally.
756 In async mode, initial data including xstats are stored internally.
758 Mode-unaware code (e.g. in search algorithms) works with transactions.
759 To keep the logic simple, multiplier is set to that value.
760 As bidirectional traffic profiles send packets in both directions,
761 they are treated as transactions with two packets (one per direction).
763 :param duration: Time expressed in seconds for how long to send traffic.
764 :param rate: Traffic rate in transactions per second.
765 :param async_call: If enabled then don't wait for all incoming traffic.
766 :type duration: float
768 :type async_call: bool
769 :raises RuntimeError: In case of T-Rex driver issue.
771 self.check_mode(TrexMode.STL)
772 p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
773 if not isinstance(duration, (float, int)):
774 duration = float(duration)
776 # TODO: Refactor the code so duration is computed only once,
777 # and both the initial and the computed durations are logged.
778 duration, _ = self._compute_duration(duration=duration, multiplier=rate)
780 command_line = OptionString().add(u"python3")
781 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
782 command_line.add(f"'{dirname}/trex_stl_profile.py'")
783 command_line.change_prefix(u"--")
784 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
785 command_line.add_with_value(
786 u"profile", f"'{dirname}/{self.traffic_profile}.py'"
788 command_line.add_with_value(u"duration", f"{duration!r}")
789 command_line.add_with_value(u"frame_size", self.frame_size)
790 command_line.add_with_value(u"rate", f"{rate!r}")
791 command_line.add_with_value(u"port_0", p_0)
792 command_line.add_with_value(u"port_1", p_1)
793 command_line.add_with_value(
794 u"traffic_directions", self.traffic_directions
796 command_line.add_if(u"async_start", async_call)
797 command_line.add_if(u"latency", self.use_latency)
798 command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
799 command_line.add_with_value(u"delay", Constants.PERF_TRIAL_STL_DELAY)
801 # TODO: This is ugly. Handle parsing better.
802 self._start_time = time.monotonic()
803 self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
804 stdout, _ = exec_cmd_no_error(
805 self._node, command_line, timeout=int(duration) + 60,
806 message=u"T-Rex STL runtime error"
811 self._target_duration = None
812 self._duration = None
813 self._received = None
818 xstats = [None, None]
820 for line in stdout.splitlines():
821 if f"Xstats snapshot {index}: " in line:
822 xstats[index] = line[19:]
826 self._xstats = tuple(xstats)
828 self._target_duration = duration
829 self._duration = duration
830 self._parse_traffic_results(stdout)
832 def send_traffic_on_tg(
840 traffic_directions=2,
841 transaction_duration=0.0,
843 transaction_type=u"packet",
847 ramp_up_duration=None,
851 """Send traffic from all configured interfaces on TG.
853 In async mode, xstats is stored internally,
854 to enable getting correct result when stopping the traffic.
855 In both modes, stdout is returned,
856 but _parse_traffic_results only works in sync output.
858 Note that traffic generator uses DPDK driver which might
859 reorder port numbers based on wiring and PCI numbering.
860 This method handles that, so argument values are invariant,
861 but you can see swapped valued in debug logs.
863 When transaction_scale is specified, the duration value is ignored
864 and the needed time is computed. For cases where this results in
865 to too long measurement (e.g. teardown trial with small rate),
866 duration_limit is applied (of non-zero), so the trial is stopped sooner.
868 Bidirectional STL profiles are treated as transactions with two packets.
870 The return value is None for async.
872 :param duration: Duration of test traffic generation in seconds.
873 :param rate: Traffic rate in transactions per second.
874 :param frame_size: Frame size (L2) in Bytes.
875 :param traffic_profile: Module name as a traffic profile identifier.
876 See GPL/traffic_profiles/trex for implemented modules.
877 :param async_call: Async mode.
878 :param ppta: Packets per transaction, aggregated over directions.
879 Needed for udp_pps which does not have a good transaction counter,
880 so we need to compute expected number of packets.
882 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
884 :param transaction_duration: Total expected time to close transaction.
885 :param transaction_scale: Number of transactions to perform.
886 0 (default) means unlimited.
887 :param transaction_type: An identifier specifying which counters
888 and formulas to use when computing attempted and failed
889 transactions. Default: "packet".
890 :param duration_limit: Zero or maximum limit for computed (or given)
892 :param use_latency: Whether to measure latency during the trial.
894 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
895 :param ramp_up_duration: Duration of ramp-up trials [s].
896 :param state_timeout: Time of life of DUT state [s].
897 :param ramp_up_only: If true, do not perform main trial measurement.
898 :type duration: float
900 :type frame_size: str
901 :type traffic_profile: str
902 :type async_call: bool
904 :type traffic_directions: int
905 :type transaction_duration: float
906 :type transaction_scale: int
907 :type transaction_type: str
908 :type duration_limit: float
909 :type use_latency: bool
910 :type ramp_up_rate: float
911 :type ramp_up_duration: float
912 :type state_timeout: float
913 :type ramp_up_only: bool
914 :returns: TG results.
915 :rtype: ReceiveRateMeasurement or None
916 :raises ValueError: If TG traffic profile is not supported.
918 self.set_rate_provider_defaults(
919 frame_size=frame_size,
920 traffic_profile=traffic_profile,
922 traffic_directions=traffic_directions,
923 transaction_duration=transaction_duration,
924 transaction_scale=transaction_scale,
925 transaction_type=transaction_type,
926 duration_limit=duration_limit,
927 use_latency=use_latency,
928 ramp_up_rate=ramp_up_rate,
929 ramp_up_duration=ramp_up_duration,
930 state_timeout=state_timeout,
932 return self._send_traffic_on_tg_with_ramp_up(
935 async_call=async_call,
936 ramp_up_only=ramp_up_only,
939 def _send_traffic_on_tg_internal(
940 self, duration, rate, async_call=False):
941 """Send traffic from all configured interfaces on TG.
943 This is an internal function, it assumes set_rate_provider_defaults
944 has been called to remember most values.
945 The reason why need to remember various values is that
946 the traffic can be asynchronous, and parsing needs those values.
947 The reason why this is is a separate function from the one
948 which calls set_rate_provider_defaults is that some search algorithms
949 need to specify their own values, and we do not want the measure call
950 to overwrite them with defaults.
952 This function is used both for automated ramp-up trials
953 and for explicitly called trials.
955 :param duration: Duration of test traffic generation in seconds.
956 :param rate: Traffic rate in transactions per second.
957 :param async_call: Async mode.
958 :type duration: float
960 :type async_call: bool
961 :returns: TG results.
962 :rtype: ReceiveRateMeasurement or None
963 :raises ValueError: If TG traffic profile is not supported.
965 subtype = check_subtype(self._node)
966 if subtype == NodeSubTypeTG.TREX:
967 if u"trex-astf" in self.traffic_profile:
968 self.trex_astf_start_remote_exec(
969 duration, float(rate), async_call
971 elif u"trex-stl" in self.traffic_profile:
972 unit_rate_str = str(rate) + u"pps"
973 # TODO: Suport transaction_scale et al?
974 self.trex_stl_start_remote_exec(
975 duration, unit_rate_str, async_call
978 raise ValueError(u"Unsupported T-Rex traffic profile!")
980 return None if async_call else self._get_measurement_result()
982 def _send_traffic_on_tg_with_ramp_up(
983 self, duration, rate, async_call=False, ramp_up_only=False):
984 """Send traffic from all interfaces on TG, maybe after ramp-up.
986 This is an internal function, it assumes set_rate_provider_defaults
987 has been called to remember most values.
988 The reason why need to remember various values is that
989 the traffic can be asynchronous, and parsing needs those values.
990 The reason why this is a separate function from the one
991 which calls set_rate_provider_defaults is that some search algorithms
992 need to specify their own values, and we do not want the measure call
993 to overwrite them with defaults.
995 If ramp-up tracking is detected, a computation is performed,
996 and if state timeout is near, trial at ramp-up rate and duration
997 is inserted before the main trial measurement.
999 The ramp_up_only parameter forces a ramp-up without immediate
1000 trial measurement, which is useful in case self remembers
1001 a previous ramp-up trial that belongs to a different test (phase).
1003 Return None if trial is async or ramp-up only.
1005 :param duration: Duration of test traffic generation in seconds.
1006 :param rate: Traffic rate in transactions per second.
1007 :param async_call: Async mode.
1008 :param ramp_up_only: If true, do not perform main trial measurement.
1009 :type duration: float
1011 :type async_call: bool
1012 :type ramp_up_only: bool
1013 :returns: TG results.
1014 :rtype: ReceiveRateMeasurement or None
1015 :raises ValueError: If TG traffic profile is not supported.
1018 if self.ramp_up_rate:
1019 # Figure out whether we need to insert a ramp-up trial.
1020 # TODO: Give up on async_call=True?
1021 if ramp_up_only or self.ramp_up_start is None:
1022 # We never ramped up yet (at least not in this test case).
1023 ramp_up_needed = True
1025 # We ramped up before, but maybe it was too long ago.
1026 # Adding a constant overhead to be safe.
1027 time_now = time.monotonic() + 1.0
1028 computed_duration, complete = self._compute_duration(
1032 # There are two conditions for inserting ramp-up.
1033 # If early sessions are expiring already,
1034 # or if late sessions are to expire before measurement is over.
1035 ramp_up_start_delay = time_now - self.ramp_up_start
1036 ramp_up_stop_delay = time_now - self.ramp_up_stop
1037 ramp_up_stop_delay += computed_duration
1038 bigger_delay = max(ramp_up_start_delay, ramp_up_stop_delay)
1039 # Final boolean decision.
1040 ramp_up_needed = (bigger_delay >= self.state_timeout)
1043 u"State may time out during next real trial, "
1044 u"inserting a ramp-up trial."
1046 self.ramp_up_start = time.monotonic()
1047 self._send_traffic_on_tg_internal(
1048 duration=self.ramp_up_duration,
1049 rate=self.ramp_up_rate,
1050 async_call=async_call,
1052 self.ramp_up_stop = time.monotonic()
1053 logger.debug(u"Ramp-up done.")
1056 u"State will probably not time out during next real trial, "
1057 u"no ramp-up trial needed just yet."
1061 trial_start = time.monotonic()
1062 result = self._send_traffic_on_tg_internal(
1065 async_call=async_call,
1067 trial_end = time.monotonic()
1068 if self.ramp_up_rate:
1069 # Optimization: No loss acts as a good ramp-up, if it was complete.
1070 if complete and result is not None and result.loss_count == 0:
1071 logger.debug(u"Good trial acts as a ramp-up")
1072 self.ramp_up_start = trial_start
1073 self.ramp_up_stop = trial_end
1075 logger.debug(u"Loss or incomplete, does not act as a ramp-up.")
1078 def no_traffic_loss_occurred(self):
1079 """Fail if loss occurred in traffic run.
1082 :raises Exception: If loss occured.
1084 if self._loss is None:
1085 raise RuntimeError(u"The traffic generation has not been issued")
1086 if self._loss != u"0":
1087 raise RuntimeError(f"Traffic loss occurred: {self._loss}")
1089 def fail_if_no_traffic_forwarded(self):
1090 """Fail if no traffic forwarded.
1092 TODO: Check number of passed transactions instead.
1095 :raises Exception: If no traffic forwarded.
1097 if self._received is None:
1098 raise RuntimeError(u"The traffic generation has not been issued")
1099 if self._received == 0:
1100 raise RuntimeError(u"No traffic forwarded")
1102 def partial_traffic_loss_accepted(
1103 self, loss_acceptance, loss_acceptance_type):
1104 """Fail if loss is higher then accepted in traffic run.
1106 :param loss_acceptance: Permitted drop ratio or frames count.
1107 :param loss_acceptance_type: Type of permitted loss.
1108 :type loss_acceptance: float
1109 :type loss_acceptance_type: LossAcceptanceType
1111 :raises Exception: If loss is above acceptance criteria.
1113 if self._loss is None:
1114 raise Exception(u"The traffic generation has not been issued")
1116 if loss_acceptance_type == u"percentage":
1117 loss = (float(self._loss) / float(self._sent)) * 100
1118 elif loss_acceptance_type == u"frames":
1119 loss = float(self._loss)
1121 raise Exception(u"Loss acceptance type not supported")
1123 if loss > float(loss_acceptance):
1125 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
1128 def _parse_traffic_results(self, stdout):
1129 """Parse stdout of scripts into fields of self.
1131 Block of code to reuse, by sync start, or stop after async.
1133 :param stdout: Text containing the standard output.
1136 subtype = check_subtype(self._node)
1137 if subtype == NodeSubTypeTG.TREX:
1138 # Last line from console output
1139 line = stdout.splitlines()[-1]
1140 results = line.split(u";")
1141 if results[-1] in (u" ", u""):
1143 self._result = dict()
1144 for result in results:
1145 key, value = result.split(u"=", maxsplit=1)
1146 self._result[key.strip()] = value
1147 logger.info(f"TrafficGen results:\n{self._result}")
1148 self._received = int(self._result.get(u"total_received"), 0)
1149 self._sent = int(self._result.get(u"total_sent", 0))
1150 self._loss = int(self._result.get(u"frame_loss", 0))
1151 self._approximated_duration = \
1152 self._result.get(u"approximated_duration", 0.0)
1153 if u"manual" not in str(self._approximated_duration):
1154 self._approximated_duration = float(self._approximated_duration)
1155 self._latency = list()
1156 self._latency.append(self._result.get(u"latency_stream_0(usec)"))
1157 self._latency.append(self._result.get(u"latency_stream_1(usec)"))
1158 if self._mode == TrexMode.ASTF:
1159 self._l7_data = dict()
1160 self._l7_data[u"client"] = dict()
1161 self._l7_data[u"client"][u"sent"] = \
1162 int(self._result.get(u"client_sent", 0))
1163 self._l7_data[u"client"][u"received"] = \
1164 int(self._result.get(u"client_received", 0))
1165 self._l7_data[u"client"][u"active_flows"] = \
1166 int(self._result.get(u"client_active_flows", 0))
1167 self._l7_data[u"client"][u"established_flows"] = \
1168 int(self._result.get(u"client_established_flows", 0))
1169 self._l7_data[u"client"][u"traffic_duration"] = \
1170 float(self._result.get(u"client_traffic_duration", 0.0))
1171 self._l7_data[u"client"][u"err_rx_throttled"] = \
1172 int(self._result.get(u"client_err_rx_throttled", 0))
1173 self._l7_data[u"client"][u"err_c_nf_throttled"] = \
1174 int(self._result.get(u"client_err_nf_throttled", 0))
1175 self._l7_data[u"client"][u"err_flow_overflow"] = \
1176 int(self._result.get(u"client_err_flow_overflow", 0))
1177 self._l7_data[u"server"] = dict()
1178 self._l7_data[u"server"][u"active_flows"] = \
1179 int(self._result.get(u"server_active_flows", 0))
1180 self._l7_data[u"server"][u"established_flows"] = \
1181 int(self._result.get(u"server_established_flows", 0))
1182 self._l7_data[u"server"][u"traffic_duration"] = \
1183 float(self._result.get(u"server_traffic_duration", 0.0))
1184 self._l7_data[u"server"][u"err_rx_throttled"] = \
1185 int(self._result.get(u"client_err_rx_throttled", 0))
1186 if u"udp" in self.traffic_profile:
1187 self._l7_data[u"client"][u"udp"] = dict()
1188 self._l7_data[u"client"][u"udp"][u"connects"] = \
1189 int(self._result.get(u"client_udp_connects", 0))
1190 self._l7_data[u"client"][u"udp"][u"closed_flows"] = \
1191 int(self._result.get(u"client_udp_closed", 0))
1192 self._l7_data[u"client"][u"udp"][u"tx_bytes"] = \
1193 int(self._result.get(u"client_udp_tx_bytes", 0))
1194 self._l7_data[u"client"][u"udp"][u"rx_bytes"] = \
1195 int(self._result.get(u"client_udp_rx_bytes", 0))
1196 self._l7_data[u"client"][u"udp"][u"tx_packets"] = \
1197 int(self._result.get(u"client_udp_tx_packets", 0))
1198 self._l7_data[u"client"][u"udp"][u"rx_packets"] = \
1199 int(self._result.get(u"client_udp_rx_packets", 0))
1200 self._l7_data[u"client"][u"udp"][u"keep_drops"] = \
1201 int(self._result.get(u"client_udp_keep_drops", 0))
1202 self._l7_data[u"client"][u"udp"][u"err_cwf"] = \
1203 int(self._result.get(u"client_err_cwf", 0))
1204 self._l7_data[u"server"][u"udp"] = dict()
1205 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = \
1206 int(self._result.get(u"server_udp_accepts", 0))
1207 self._l7_data[u"server"][u"udp"][u"closed_flows"] = \
1208 int(self._result.get(u"server_udp_closed", 0))
1209 self._l7_data[u"server"][u"udp"][u"tx_bytes"] = \
1210 int(self._result.get(u"server_udp_tx_bytes", 0))
1211 self._l7_data[u"server"][u"udp"][u"rx_bytes"] = \
1212 int(self._result.get(u"server_udp_rx_bytes", 0))
1213 self._l7_data[u"server"][u"udp"][u"tx_packets"] = \
1214 int(self._result.get(u"server_udp_tx_packets", 0))
1215 self._l7_data[u"server"][u"udp"][u"rx_packets"] = \
1216 int(self._result.get(u"server_udp_rx_packets", 0))
1217 elif u"tcp" in self.traffic_profile:
1218 self._l7_data[u"client"][u"tcp"] = dict()
1219 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = \
1220 int(self._result.get(u"client_tcp_connect_inits", 0))
1221 self._l7_data[u"client"][u"tcp"][u"connects"] = \
1222 int(self._result.get(u"client_tcp_connects", 0))
1223 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = \
1224 int(self._result.get(u"client_tcp_closed", 0))
1225 self._l7_data[u"client"][u"tcp"][u"connattempt"] = \
1226 int(self._result.get(u"client_tcp_connattempt", 0))
1227 self._l7_data[u"client"][u"tcp"][u"tx_bytes"] = \
1228 int(self._result.get(u"client_tcp_tx_bytes", 0))
1229 self._l7_data[u"client"][u"tcp"][u"rx_bytes"] = \
1230 int(self._result.get(u"client_tcp_rx_bytes", 0))
1231 self._l7_data[u"server"][u"tcp"] = dict()
1232 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = \
1233 int(self._result.get(u"server_tcp_accepts", 0))
1234 self._l7_data[u"server"][u"tcp"][u"connects"] = \
1235 int(self._result.get(u"server_tcp_connects", 0))
1236 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = \
1237 int(self._result.get(u"server_tcp_closed", 0))
1238 self._l7_data[u"server"][u"tcp"][u"tx_bytes"] = \
1239 int(self._result.get(u"server_tcp_tx_bytes", 0))
1240 self._l7_data[u"server"][u"tcp"][u"rx_bytes"] = \
1241 int(self._result.get(u"server_tcp_rx_bytes", 0))
1243 def _get_measurement_result(self):
1244 """Return the result of last measurement as ReceiveRateMeasurement.
1246 Separate function, as measurements can end either by time
1247 or by explicit call, this is the common block at the end.
1249 The target_tr field of ReceiveRateMeasurement is in
1250 transactions per second. Transmit count and loss count units
1251 depend on the transaction type. Usually they are in transactions
1252 per second, or aggregated packets per second.
1254 TODO: Fail on running or already reported measurement.
1256 :returns: Structure containing the result of the measurement.
1257 :rtype: ReceiveRateMeasurement
1260 # Client duration seems to include a setup period
1261 # where TRex does not send any packets yet.
1262 # Server duration does not include it.
1263 server_data = self._l7_data[u"server"]
1264 approximated_duration = float(server_data[u"traffic_duration"])
1265 except (KeyError, AttributeError, ValueError, TypeError):
1266 approximated_duration = None
1268 if not approximated_duration:
1269 approximated_duration = float(self._approximated_duration)
1270 except ValueError: # "manual"
1271 approximated_duration = None
1272 if not approximated_duration:
1273 if self._duration and self._duration > 0:
1274 # Known recomputed or target duration.
1275 approximated_duration = self._duration
1277 # It was an explicit stop.
1278 if not self._stop_time:
1279 raise RuntimeError(u"Unable to determine duration.")
1280 approximated_duration = self._stop_time - self._start_time
1281 target_duration = self._target_duration
1282 if not target_duration:
1283 target_duration = approximated_duration
1284 transmit_rate = self._rate
1286 if self.transaction_type == u"packet":
1287 partial_attempt_count = self._sent
1288 packet_rate = transmit_rate * self.ppta
1289 # We have a float. TRex way of rounding it is not obvious.
1290 # The biggest source of mismatch is Inter Stream Gap.
1291 # So the code tolerates 10 usec of missing packets.
1292 expected_attempt_count = (target_duration - 1e-5) * packet_rate
1293 expected_attempt_count = math.ceil(expected_attempt_count)
1294 # TRex can send more.
1295 expected_attempt_count = max(expected_attempt_count, self._sent)
1296 unsent = expected_attempt_count - self._sent
1297 pass_count = self._received
1298 fail_count = expected_attempt_count - pass_count
1299 elif self.transaction_type == u"udp_cps":
1300 if not self.transaction_scale:
1301 raise RuntimeError(u"Add support for no-limit udp_cps.")
1302 partial_attempt_count = self._l7_data[u"client"][u"sent"]
1303 # We do not care whether TG is slow, it should have attempted all.
1304 expected_attempt_count = self.transaction_scale
1305 unsent = expected_attempt_count - partial_attempt_count
1306 pass_count = self._l7_data[u"client"][u"received"]
1307 fail_count = expected_attempt_count - pass_count
1308 elif self.transaction_type == u"tcp_cps":
1309 if not self.transaction_scale:
1310 raise RuntimeError(u"Add support for no-limit tcp_cps.")
1311 ctca = self._l7_data[u"client"][u"tcp"][u"connattempt"]
1312 partial_attempt_count = ctca
1313 # We do not care whether TG is slow, it should have attempted all.
1314 expected_attempt_count = self.transaction_scale
1315 unsent = expected_attempt_count - partial_attempt_count
1316 # From TCP point of view, server/connects counts full connections,
1317 # but we are testing NAT session so client/connects counts that
1318 # (half connections from TCP point of view).
1319 pass_count = self._l7_data[u"client"][u"tcp"][u"connects"]
1320 fail_count = expected_attempt_count - pass_count
1321 elif self.transaction_type == u"udp_pps":
1322 if not self.transaction_scale:
1323 raise RuntimeError(u"Add support for no-limit udp_pps.")
1324 partial_attempt_count = self._sent
1325 expected_attempt_count = self.transaction_scale * self.ppta
1326 unsent = expected_attempt_count - self._sent
1327 fail_count = self._loss + unsent
1328 elif self.transaction_type == u"tcp_pps":
1329 if not self.transaction_scale:
1330 raise RuntimeError(u"Add support for no-limit tcp_pps.")
1331 partial_attempt_count = self._sent
1332 expected_attempt_count = self.transaction_scale * self.ppta
1333 # One loss-like scenario happens when TRex receives all packets
1334 # on L2 level, but is not fast enough to process them all
1335 # at L7 level, which leads to retransmissions.
1336 # Those manifest as opackets larger than expected.
1337 # A simple workaround is to add absolute difference.
1338 # Probability of retransmissions exactly cancelling
1339 # packets unsent due to duration stretching is quite low.
1340 unsent = abs(expected_attempt_count - self._sent)
1341 fail_count = self._loss + unsent
1343 raise RuntimeError(f"Unknown parsing {self.transaction_type!r}")
1344 if unsent and isinstance(self._approximated_duration, float):
1345 # Do not report unsent for "manual".
1346 logger.debug(f"Unsent packets/transactions: {unsent}")
1347 if fail_count < 0 and not self.negative_loss:
1349 measurement = ReceiveRateMeasurement(
1350 duration=target_duration,
1351 target_tr=transmit_rate,
1352 transmit_count=expected_attempt_count,
1353 loss_count=fail_count,
1354 approximated_duration=approximated_duration,
1355 partial_transmit_count=partial_attempt_count,
1357 measurement.latency = self.get_latency_int()
1360 def measure(self, duration, transmit_rate):
1361 """Run trial measurement, parse and return results.
1363 The input rate is for transactions. Stateles bidirectional traffic
1364 is understood as sequence of (asynchronous) transactions,
1367 The result units depend on test type, generally
1368 the count either transactions or packets (aggregated over directions).
1370 Optionally, this method sleeps if measurement finished before
1371 the time specified as duration.
1373 :param duration: Trial duration [s].
1374 :param transmit_rate: Target rate in transactions per second.
1375 :type duration: float
1376 :type transmit_rate: float
1377 :returns: Structure containing the result of the measurement.
1378 :rtype: ReceiveRateMeasurement
1379 :raises RuntimeError: If TG is not set or if node is not TG
1380 or if subtype is not specified.
1381 :raises NotImplementedError: If TG is not supported.
1383 duration = float(duration)
1384 time_start = time.monotonic()
1385 time_stop = time_start + duration
1388 result = self._send_traffic_on_tg_with_ramp_up(
1393 logger.debug(f"trial measurement result: {result!r}")
1394 # In PLRsearch, computation needs the specified time to complete.
1395 if self.sleep_till_duration:
1396 sleeptime = time_stop - time.monotonic()
1398 # TODO: Sometimes we have time to do additional trials here,
1399 # adapt PLRsearch to accept all the results.
1400 time.sleep(sleeptime)
1403 def set_rate_provider_defaults(
1409 traffic_directions=2,
1410 transaction_duration=0.0,
1411 transaction_scale=0,
1412 transaction_type=u"packet",
1415 sleep_till_duration=False,
1418 ramp_up_duration=None,
1419 state_timeout=240.0,
1421 """Store values accessed by measure().
1423 :param frame_size: Frame size identifier or value [B].
1424 :param traffic_profile: Module name as a traffic profile identifier.
1425 See GPL/traffic_profiles/trex for implemented modules.
1426 :param ppta: Packets per transaction, aggregated over directions.
1427 Needed for udp_pps which does not have a good transaction counter,
1428 so we need to compute expected number of packets.
1430 :param resetter: Callable to reset DUT state for repeated trials.
1431 :param traffic_directions: Traffic from packet counting point of view
1432 is bi- (2) or uni- (1) directional.
1434 :param transaction_duration: Total expected time to close transaction.
1435 :param transaction_scale: Number of transactions to perform.
1436 0 (default) means unlimited.
1437 :param transaction_type: An identifier specifying which counters
1438 and formulas to use when computing attempted and failed
1439 transactions. Default: "packet".
1440 TODO: Does this also specify parsing for the measured duration?
1441 :param duration_limit: Zero or maximum limit for computed (or given)
1443 :param negative_loss: If false, negative loss is reported as zero loss.
1444 :param sleep_till_duration: If true and measurement returned faster,
1445 sleep until it matches duration. Needed for PLRsearch.
1446 :param use_latency: Whether to measure latency during the trial.
1448 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1449 :param ramp_up_duration: Duration of ramp-up trials [s].
1450 :param state_timeout: Time of life of DUT state [s].
1451 :type frame_size: str or int
1452 :type traffic_profile: str
1454 :type resetter: Optional[Callable[[], None]]
1455 :type traffic_directions: int
1456 :type transaction_duration: float
1457 :type transaction_scale: int
1458 :type transaction_type: str
1459 :type duration_limit: float
1460 :type negative_loss: bool
1461 :type sleep_till_duration: bool
1462 :type use_latency: bool
1463 :type ramp_up_rate: float
1464 :type ramp_up_duration: float
1465 :type state_timeout: float
1467 self.frame_size = frame_size
1468 self.traffic_profile = str(traffic_profile)
1469 self.resetter = resetter
1471 self.traffic_directions = int(traffic_directions)
1472 self.transaction_duration = float(transaction_duration)
1473 self.transaction_scale = int(transaction_scale)
1474 self.transaction_type = str(transaction_type)
1475 self.duration_limit = float(duration_limit)
1476 self.negative_loss = bool(negative_loss)
1477 self.sleep_till_duration = bool(sleep_till_duration)
1478 self.use_latency = bool(use_latency)
1479 self.ramp_up_rate = float(ramp_up_rate)
1480 self.ramp_up_duration = float(ramp_up_duration)
1481 self.state_timeout = float(state_timeout)
1484 class OptimizedSearch:
1485 """Class to be imported as Robot Library, containing search keywords.
1487 Aside of setting up measurer and forwarding arguments,
1488 the main business is to translate min/max rate from unidir to aggregated.
1492 def perform_optimized_ndrpdr_search(
1495 minimum_transmit_rate,
1496 maximum_transmit_rate,
1497 packet_loss_ratio=0.005,
1498 final_relative_width=0.005,
1499 final_trial_duration=30.0,
1500 initial_trial_duration=1.0,
1501 number_of_intermediate_phases=2,
1505 traffic_directions=2,
1506 transaction_duration=0.0,
1507 transaction_scale=0,
1508 transaction_type=u"packet",
1511 ramp_up_duration=None,
1512 state_timeout=240.0,
1513 expansion_coefficient=4.0,
1515 """Setup initialized TG, perform optimized search, return intervals.
1517 If transaction_scale is nonzero, all init and non-init trial durations
1518 are set to 1.0 (as they do not affect the real trial duration)
1519 and zero intermediate phases are used.
1520 This way no re-measurement happens.
1521 Warmup has to be handled via resetter or ramp-up mechanisms.
1523 :param frame_size: Frame size identifier or value [B].
1524 :param traffic_profile: Module name as a traffic profile identifier.
1525 See GPL/traffic_profiles/trex for implemented modules.
1526 :param minimum_transmit_rate: Minimal load in transactions per second.
1527 :param maximum_transmit_rate: Maximal load in transactions per second.
1528 :param packet_loss_ratio: Ratio of packets lost, for PDR [1].
1529 :param final_relative_width: Final lower bound transmit rate
1530 cannot be more distant that this multiple of upper bound [1].
1531 :param final_trial_duration: Trial duration for the final phase [s].
1532 :param initial_trial_duration: Trial duration for the initial phase
1533 and also for the first intermediate phase [s].
1534 :param number_of_intermediate_phases: Number of intermediate phases
1535 to perform before the final phase [1].
1536 :param timeout: The search will fail itself when not finished
1537 before this overall time [s].
1538 :param ppta: Packets per transaction, aggregated over directions.
1539 Needed for udp_pps which does not have a good transaction counter,
1540 so we need to compute expected number of packets.
1542 :param resetter: Callable to reset DUT state for repeated trials.
1543 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1545 :param transaction_duration: Total expected time to close transaction.
1546 :param transaction_scale: Number of transactions to perform.
1547 0 (default) means unlimited.
1548 :param transaction_type: An identifier specifying which counters
1549 and formulas to use when computing attempted and failed
1550 transactions. Default: "packet".
1551 :param use_latency: Whether to measure latency during the trial.
1553 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1554 :param ramp_up_duration: Duration of ramp-up trials [s].
1555 :param state_timeout: Time of life of DUT state [s].
1556 :param expansion_coefficient: In external search multiply width by this.
1557 :type frame_size: str or int
1558 :type traffic_profile: str
1559 :type minimum_transmit_rate: float
1560 :type maximum_transmit_rate: float
1561 :type packet_loss_ratio: float
1562 :type final_relative_width: float
1563 :type final_trial_duration: float
1564 :type initial_trial_duration: float
1565 :type number_of_intermediate_phases: int
1566 :type timeout: float
1568 :type resetter: Optional[Callable[[], None]]
1569 :type traffic_directions: int
1570 :type transaction_duration: float
1571 :type transaction_scale: int
1572 :type transaction_type: str
1573 :type use_latency: bool
1574 :type ramp_up_rate: float
1575 :type ramp_up_duration: float
1576 :type state_timeout: float
1577 :type expansion_coefficient: float
1578 :returns: Structure containing narrowed down NDR and PDR intervals
1579 and their measurements.
1580 :rtype: List[Receiverateinterval]
1581 :raises RuntimeError: If total duration is larger than timeout.
1583 # we need instance of TrafficGenerator instantiated by Robot Framework
1584 # to be able to use trex_stl-*()
1585 tg_instance = BuiltIn().get_library_instance(
1586 u"resources.libraries.python.TrafficGenerator"
1588 # Overrides for fixed transaction amount.
1589 # TODO: Move to robot code? We have two call sites, so this saves space,
1590 # even though this is surprising for log readers.
1591 if transaction_scale:
1592 initial_trial_duration = 1.0
1593 final_trial_duration = 1.0
1594 number_of_intermediate_phases = 0
1595 timeout += transaction_scale * 3e-4
1596 tg_instance.set_rate_provider_defaults(
1597 frame_size=frame_size,
1598 traffic_profile=traffic_profile,
1599 sleep_till_duration=False,
1602 traffic_directions=traffic_directions,
1603 transaction_duration=transaction_duration,
1604 transaction_scale=transaction_scale,
1605 transaction_type=transaction_type,
1606 use_latency=use_latency,
1607 ramp_up_rate=ramp_up_rate,
1608 ramp_up_duration=ramp_up_duration,
1609 state_timeout=state_timeout,
1611 algorithm = MultipleLossRatioSearch(
1612 measurer=tg_instance,
1613 final_trial_duration=final_trial_duration,
1614 final_relative_width=final_relative_width,
1615 number_of_intermediate_phases=number_of_intermediate_phases,
1616 initial_trial_duration=initial_trial_duration,
1619 expansion_coefficient=expansion_coefficient,
1621 if packet_loss_ratio:
1622 packet_loss_ratios = [0.0, packet_loss_ratio]
1624 # Happens in reconf tests.
1625 packet_loss_ratios = [packet_loss_ratio]
1626 results = algorithm.narrow_down_intervals(
1627 min_rate=minimum_transmit_rate,
1628 max_rate=maximum_transmit_rate,
1629 packet_loss_ratios=packet_loss_ratios,
1634 def perform_soak_search(
1637 minimum_transmit_rate,
1638 maximum_transmit_rate,
1645 trace_enabled=False,
1646 traffic_directions=2,
1647 transaction_duration=0.0,
1648 transaction_scale=0,
1649 transaction_type=u"packet",
1652 ramp_up_duration=None,
1653 state_timeout=240.0,
1655 """Setup initialized TG, perform soak search, return avg and stdev.
1657 :param frame_size: Frame size identifier or value [B].
1658 :param traffic_profile: Module name as a traffic profile identifier.
1659 See GPL/traffic_profiles/trex for implemented modules.
1660 :param minimum_transmit_rate: Minimal load in transactions per second.
1661 :param maximum_transmit_rate: Maximal load in transactions per second.
1662 :param plr_target: Ratio of packets lost to achieve [1].
1663 :param tdpt: Trial duration per trial.
1664 The algorithm linearly increases trial duration with trial number,
1665 this is the increment between succesive trials, in seconds.
1666 :param initial_count: Offset to apply before the first trial.
1667 For example initial_count=50 makes first trial to be 51*tdpt long.
1668 This is needed because initial "search" phase of integrator
1669 takes significant time even without any trial results.
1670 :param timeout: The search will stop after this overall time [s].
1671 :param ppta: Packets per transaction, aggregated over directions.
1672 Needed for udp_pps which does not have a good transaction counter,
1673 so we need to compute expected number of packets.
1675 :param resetter: Callable to reset DUT state for repeated trials.
1676 :param trace_enabled: True if trace enabled else False.
1677 This is very verbose tracing on numeric computations,
1678 do not use in production.
1680 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1682 :param transaction_duration: Total expected time to close transaction.
1683 :param transaction_scale: Number of transactions to perform.
1684 0 (default) means unlimited.
1685 :param transaction_type: An identifier specifying which counters
1686 and formulas to use when computing attempted and failed
1687 transactions. Default: "packet".
1688 :param use_latency: Whether to measure latency during the trial.
1690 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1691 :param ramp_up_duration: Duration of ramp-up trials [s].
1692 :param state_timeout: Time of life of DUT state [s].
1693 :type frame_size: str or int
1694 :type traffic_profile: str
1695 :type minimum_transmit_rate: float
1696 :type maximum_transmit_rate: float
1697 :type plr_target: float
1698 :type initial_count: int
1699 :type timeout: float
1701 :type resetter: Optional[Callable[[], None]]
1702 :type trace_enabled: bool
1703 :type traffic_directions: int
1704 :type transaction_duration: float
1705 :type transaction_scale: int
1706 :type transaction_type: str
1707 :type use_latency: bool
1708 :type ramp_up_rate: float
1709 :type ramp_up_duration: float
1710 :type state_timeout: float
1711 :returns: Average and stdev of estimated aggregated rate giving PLR.
1712 :rtype: 2-tuple of float
1714 tg_instance = BuiltIn().get_library_instance(
1715 u"resources.libraries.python.TrafficGenerator"
1717 # Overrides for fixed transaction amount.
1718 # TODO: Move to robot code? We have a single call site
1719 # but MLRsearch has two and we want the two to be used similarly.
1720 if transaction_scale:
1721 # TODO: What is a good value for max scale?
1722 # TODO: Scale the timeout with transaction scale.
1724 tg_instance.set_rate_provider_defaults(
1725 frame_size=frame_size,
1726 traffic_profile=traffic_profile,
1727 negative_loss=False,
1728 sleep_till_duration=True,
1731 traffic_directions=traffic_directions,
1732 transaction_duration=transaction_duration,
1733 transaction_scale=transaction_scale,
1734 transaction_type=transaction_type,
1735 use_latency=use_latency,
1736 ramp_up_rate=ramp_up_rate,
1737 ramp_up_duration=ramp_up_duration,
1738 state_timeout=state_timeout,
1740 algorithm = PLRsearch(
1741 measurer=tg_instance,
1742 trial_duration_per_trial=tdpt,
1743 packet_loss_ratio_target=plr_target,
1744 trial_number_offset=initial_count,
1746 trace_enabled=trace_enabled,
1748 result = algorithm.search(
1749 min_rate=minimum_transmit_rate,
1750 max_rate=maximum_transmit_rate,