1 # Copyright (c) 2023 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
34 from .DUTSetup import DUTSetup as DS
36 __all__ = [u"TGDropRateSearchImpl", u"TrafficGenerator", u"OptimizedSearch"]
39 def check_subtype(node):
40 """Return supported subtype of given node, or raise an exception.
42 Currently only one subtype is supported,
43 but we want our code to be ready for other ones.
45 :param node: Topology node to check. Can be None.
46 :type node: dict or NoneType
47 :returns: Subtype detected.
49 :raises RuntimeError: If node is not supported, message explains how.
51 if node.get(u"type") is None:
52 msg = u"Node type is not defined"
53 elif node[u"type"] != NodeType.TG:
54 msg = f"Node type is {node[u'type']!r}, not a TG"
55 elif node.get(u"subtype") is None:
56 msg = u"TG subtype is not defined"
57 elif node[u"subtype"] != NodeSubTypeTG.TREX:
58 msg = f"TG subtype {node[u'subtype']!r} is not supported"
60 return NodeSubTypeTG.TREX
61 raise RuntimeError(msg)
64 class TGDropRateSearchImpl(DropRateSearch):
65 """Drop Rate Search implementation."""
68 # super(TGDropRateSearchImpl, self).__init__()
71 self, rate, frame_size, loss_acceptance, loss_acceptance_type,
73 """Runs the traffic and evaluate the measured results.
75 :param rate: Offered traffic load.
76 :param frame_size: Size of frame.
77 :param loss_acceptance: Permitted drop ratio or frames count.
78 :param loss_acceptance_type: Type of permitted loss.
79 :param traffic_profile: Module name as a traffic profile identifier.
80 See GPL/traffic_profiles/trex for implemented modules.
83 :type loss_acceptance: float
84 :type loss_acceptance_type: LossAcceptanceType
85 :type traffic_profile: str
86 :returns: Drop threshold exceeded? (True/False)
88 :raises NotImplementedError: If TG is not supported.
89 :raises RuntimeError: If TG is not specified.
91 # we need instance of TrafficGenerator instantiated by Robot Framework
92 # to be able to use trex_stl-*()
93 tg_instance = BuiltIn().get_library_instance(
94 u"resources.libraries.python.TrafficGenerator"
96 subtype = check_subtype(tg_instance.node)
97 if subtype == NodeSubTypeTG.TREX:
98 unit_rate = str(rate) + self.get_rate_type_str()
99 tg_instance.trex_stl_start_remote_exec(
100 self.get_duration(), unit_rate, frame_size, traffic_profile
102 loss = tg_instance.get_loss()
103 sent = tg_instance.get_sent()
104 if self.loss_acceptance_type_is_percentage():
105 loss = (float(loss) / float(sent)) * 100
107 f"comparing: {loss} < {loss_acceptance} {loss_acceptance_type}"
109 return float(loss) <= float(loss_acceptance)
112 def get_latency(self):
113 """Returns min/avg/max latency.
115 :returns: Latency stats.
118 tg_instance = BuiltIn().get_library_instance(
119 u"resources.libraries.python.TrafficGenerator"
121 return tg_instance.get_latency_int()
125 """Defines mode of T-Rex traffic generator."""
126 # Advanced stateful mode
132 # TODO: Pylint says too-many-instance-attributes.
133 class TrafficGenerator(AbstractMeasurer):
134 """Traffic Generator."""
136 # TODO: Remove "trex" from lines which could work with other TGs.
138 # Use one instance of TrafficGenerator for all tests in test suite
139 ROBOT_LIBRARY_SCOPE = u"TEST SUITE"
142 # TODO: Separate into few dataclasses/dicts.
143 # Pylint dislikes large unstructured state, and it is right.
146 # TG interface order mapping
147 self._ifaces_reordered = False
148 # Result holding fields, to be removed.
153 self._received = None
154 self._approximated_rate = None
155 self._approximated_duration = None
157 # Measurement input fields, needed for async stop result.
158 self._start_time = None
159 self._stop_time = None
161 self._target_duration = None
162 self._duration = None
163 # Other input parameters, not knowable from measure() signature.
164 self.frame_size = None
165 self.traffic_profile = None
166 self.traffic_directions = None
167 self.negative_loss = None
168 self.use_latency = None
171 self.transaction_scale = None
172 self.transaction_duration = None
173 self.sleep_till_duration = None
174 self.transaction_type = None
175 self.duration_limit = None
176 self.ramp_up_start = None
177 self.ramp_up_stop = None
178 self.ramp_up_rate = None
179 self.ramp_up_duration = None
180 self.state_timeout = None
181 # Transient data needed for async measurements.
182 self._xstats = (None, None)
183 # TODO: Rename "xstats" to something opaque, so T-Rex is not privileged?
189 :returns: Traffic generator node.
195 """Return number of lost packets.
197 :returns: Number of lost packets.
203 """Return number of sent packets.
205 :returns: Number of sent packets.
210 def get_received(self):
211 """Return number of received packets.
213 :returns: Number of received packets.
216 return self._received
218 def get_latency_int(self):
219 """Return rounded min/avg/max latency.
221 :returns: Latency stats.
226 def get_approximated_rate(self):
227 """Return approximated rate computed as ratio of transmitted packets
228 over duration of trial.
230 :returns: Approximated rate.
233 return self._approximated_rate
235 def get_l7_data(self):
238 :returns: Number of received packets.
243 def check_mode(self, expected_mode):
246 :param expected_mode: Expected traffic generator mode.
247 :type expected_mode: object
248 :raises RuntimeError: In case of unexpected TG mode.
250 if self._mode == expected_mode:
253 f"{self._node[u'subtype']} not running in {expected_mode} mode!"
257 def get_tg_type(tg_node):
258 """Log and return the installed traffic generator type.
260 :param tg_node: Node from topology file.
262 :returns: Traffic generator type string.
264 :raises RuntimeError: If command returns nonzero return code.
266 return str(check_subtype(tg_node))
269 def get_tg_version(tg_node):
270 """Log and return the installed traffic generator version.
272 :param tg_node: Node from topology file.
274 :returns: Traffic generator version string.
276 :raises RuntimeError: If command returns nonzero return code.
278 subtype = check_subtype(tg_node)
279 if subtype == NodeSubTypeTG.TREX:
280 command = f"cat {Constants.TREX_INSTALL_DIR}/VERSION"
281 message = u"Get T-Rex version failed!"
282 stdout, _ = exec_cmd_no_error(tg_node, command, message=message)
283 return stdout.strip()
287 # TODO: pylint disable=too-many-locals.
288 def initialize_traffic_generator(
289 self, tg_node, tg_if1, tg_if2, tg_if1_adj_node, tg_if1_adj_if,
290 tg_if2_adj_node, tg_if2_adj_if, osi_layer, tg_if1_dst_mac=None,
291 tg_if2_dst_mac=None):
292 """TG initialization.
294 TODO: Document why do we need (and how do we use) _ifaces_reordered.
296 :param tg_node: Traffic generator node.
297 :param tg_if1: TG - name of first interface.
298 :param tg_if2: TG - name of second interface.
299 :param tg_if1_adj_node: TG if1 adjecent node.
300 :param tg_if1_adj_if: TG if1 adjecent interface.
301 :param tg_if2_adj_node: TG if2 adjecent node.
302 :param tg_if2_adj_if: TG if2 adjecent interface.
303 :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
304 :param tg_if1_dst_mac: Interface 1 destination MAC address.
305 :param tg_if2_dst_mac: Interface 2 destination MAC address.
309 :type tg_if1_adj_node: dict
310 :type tg_if1_adj_if: str
311 :type tg_if2_adj_node: dict
312 :type tg_if2_adj_if: str
314 :type tg_if1_dst_mac: str
315 :type tg_if2_dst_mac: str
317 :raises RuntimeError: In case of issue during initialization.
319 subtype = check_subtype(tg_node)
320 if subtype == NodeSubTypeTG.TREX:
322 self._mode = TrexMode.ASTF if osi_layer == u"L7" else TrexMode.STL
325 if1[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if1)
326 if2[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if2)
327 if1[u"addr"] = Topology().get_interface_mac(self._node, tg_if1)
328 if2[u"addr"] = Topology().get_interface_mac(self._node, tg_if2)
330 if osi_layer == u"L2":
331 if1[u"adj_addr"] = if2[u"addr"]
332 if2[u"adj_addr"] = if1[u"addr"]
333 elif osi_layer in (u"L3", u"L7"):
334 if1[u"adj_addr"] = Topology().get_interface_mac(
335 tg_if1_adj_node, tg_if1_adj_if
337 if2[u"adj_addr"] = Topology().get_interface_mac(
338 tg_if2_adj_node, tg_if2_adj_if
341 raise ValueError(u"Unknown OSI layer!")
343 # in case of switched environment we can override MAC addresses
344 if tg_if1_dst_mac is not None and tg_if2_dst_mac is not None:
345 if1[u"adj_addr"] = tg_if1_dst_mac
346 if2[u"adj_addr"] = tg_if2_dst_mac
348 if min(if1[u"pci"], if2[u"pci"]) != if1[u"pci"]:
350 self._ifaces_reordered = True
352 master_thread_id, latency_thread_id, socket, threads = \
353 CpuUtils.get_affinity_trex(
354 self._node, tg_if1, tg_if2,
355 tg_dtc=Constants.TREX_CORE_COUNT)
357 if osi_layer in (u"L2", u"L3", u"L7"):
360 f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
362 f" c: {len(threads)}\n"
363 f" limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
364 f" interfaces: [\"{if1[u'pci']}\",\"{if2[u'pci']}\"]\n"
366 f" - dest_mac: \'{if1[u'adj_addr']}\'\n"
367 f" src_mac: \'{if1[u'addr']}\'\n"
368 f" - dest_mac: \'{if2[u'adj_addr']}\'\n"
369 f" src_mac: \'{if2[u'addr']}\'\n"
371 f" master_thread_id: {master_thread_id}\n"
372 f" latency_thread_id: {latency_thread_id}\n"
374 f" - socket: {socket}\n"
375 f" threads: {threads}\n"
377 sudo=True, message=u"T-Rex config generation!"
380 if Constants.TREX_RX_DESCRIPTORS_COUNT != 0:
383 f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
384 f" rx_desc: {Constants.TREX_RX_DESCRIPTORS_COUNT}\n"
386 sudo=True, message=u"T-Rex rx_desc modification!"
389 if Constants.TREX_TX_DESCRIPTORS_COUNT != 0:
392 f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
393 f" tx_desc: {Constants.TREX_TX_DESCRIPTORS_COUNT}\n"
395 sudo=True, message=u"T-Rex tx_desc modification!"
398 raise ValueError(u"Unknown OSI layer!")
400 TrafficGenerator.startup_trex(
401 self._node, osi_layer, subtype=subtype
405 def startup_trex(tg_node, osi_layer, subtype=None):
406 """Startup sequence for the TRex traffic generator.
408 :param tg_node: Traffic generator node.
409 :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
410 :param subtype: Traffic generator sub-type.
413 :type subtype: NodeSubTypeTG
414 :raises RuntimeError: If T-Rex startup failed.
415 :raises ValueError: If OSI layer is not supported.
418 subtype = check_subtype(tg_node)
419 if subtype == NodeSubTypeTG.TREX:
420 for _ in range(0, 3):
421 # Kill TRex only if it is already running.
422 cmd = u"sh -c \"pgrep t-rex && pkill t-rex && sleep 3 || true\""
424 tg_node, cmd, sudo=True, message=u"Kill TRex failed!"
427 # Prepare interfaces for TRex.
428 tg_port_drv = Constants.TREX_PORT_DRIVER
430 for port in tg_node[u"interfaces"].values():
431 if u"Mellanox" in port.get(u"model"):
432 mlx_driver = port.get(u"driver")
433 pci_addr = port.get(u'pci_address')
434 cur_driver = DS.get_pci_dev_driver(tg_node, pci_addr)
435 if cur_driver == mlx_driver:
438 DS.pci_driver_bind(tg_node, pci_addr, mlx_driver)
440 DS.pci_driver_unbind(tg_node, pci_addr)
441 DS.pci_driver_bind(tg_node, pci_addr, mlx_driver)
443 pci_addr = port.get(u'pci_address')
444 cur_driver = DS.get_pci_dev_driver(tg_node, pci_addr)
446 DS.pci_driver_unbind(tg_node, pci_addr)
447 DS.pci_driver_bind(tg_node, pci_addr, tg_port_drv)
450 cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
451 trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
453 trex_cmd.add(u"--prefix $(hostname)")
454 trex_cmd.add(u"--hdrh")
455 trex_cmd.add(u"--no-scapy-server")
456 trex_cmd.add_if(u"--astf", osi_layer == u"L7")
457 # OptionString does not create double space if extra is empty.
458 trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
459 inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
460 cmd = f"sh -c \"{inner_command}\" > /dev/null"
462 exec_cmd_no_error(tg_node, cmd, sudo=True)
464 cmd = u"sh -c \"cat /tmp/trex.log\""
466 tg_node, cmd, sudo=True,
467 message=u"Get TRex logs failed!"
469 raise RuntimeError(u"Start TRex failed!")
471 # Test T-Rex API responsiveness.
472 cmd = f"python3 {Constants.REMOTE_FW_DIR}/GPL/tools/trex/"
473 if osi_layer in (u"L2", u"L3"):
474 cmd += u"trex_stl_assert.py"
475 elif osi_layer == u"L7":
476 cmd += u"trex_astf_assert.py"
478 raise ValueError(u"Unknown OSI layer!")
481 tg_node, cmd, sudo=True,
482 message=u"T-Rex API is not responding!", retries=20
487 # After max retries TRex is still not responding to API critical
489 exec_cmd(tg_node, u"cat /tmp/trex.log", sudo=True)
490 raise RuntimeError(u"Start T-Rex failed after multiple retries!")
493 def is_trex_running(node):
494 """Check if T-Rex is running using pidof.
496 :param node: Traffic generator node.
498 :returns: True if T-Rex is running otherwise False.
501 ret, _, _ = exec_cmd(node, u"pgrep t-rex", sudo=True)
502 return bool(int(ret) == 0)
505 def teardown_traffic_generator(node):
508 :param node: Traffic generator node.
511 :raises RuntimeError: If node type is not a TG,
512 or if T-Rex teardown fails.
514 subtype = check_subtype(node)
515 if subtype == NodeSubTypeTG.TREX:
519 u"\"if pgrep t-rex; then sudo pkill t-rex && sleep 3; fi\"",
521 message=u"T-Rex kill failed!"
524 def trex_astf_stop_remote_exec(self, node):
525 """Execute T-Rex ASTF script on remote node over ssh to stop running
528 Internal state is updated with measurement results.
530 :param node: T-Rex generator node.
532 :raises RuntimeError: If stop traffic script fails.
534 command_line = OptionString().add(u"python3")
535 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
536 command_line.add(f"'{dirname}/trex_astf_stop.py'")
537 command_line.change_prefix(u"--")
538 for index, value in enumerate(self._xstats):
539 if value is not None:
540 value = value.replace(u"'", u"\"")
541 command_line.add_equals(f"xstat{index}", f"'{value}'")
542 stdout, _ = exec_cmd_no_error(
544 message=u"T-Rex ASTF runtime error!"
546 self._parse_traffic_results(stdout)
548 def trex_stl_stop_remote_exec(self, node):
549 """Execute T-Rex STL script on remote node over ssh to stop running
552 Internal state is updated with measurement results.
554 :param node: T-Rex generator node.
556 :raises RuntimeError: If stop traffic script fails.
558 command_line = OptionString().add(u"python3")
559 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
560 command_line.add(f"'{dirname}/trex_stl_stop.py'")
561 command_line.change_prefix(u"--")
562 for index, value in enumerate(self._xstats):
563 if value is not None:
564 value = value.replace(u"'", u"\"")
565 command_line.add_equals(f"xstat{index}", f"'{value}'")
566 stdout, _ = exec_cmd_no_error(
568 message=u"T-Rex STL runtime error!"
570 self._parse_traffic_results(stdout)
572 def stop_traffic_on_tg(self):
573 """Stop all traffic on TG.
575 :returns: Structure containing the result of the measurement.
576 :rtype: ReceiveRateMeasurement
577 :raises ValueError: If TG traffic profile is not supported.
579 subtype = check_subtype(self._node)
580 if subtype != NodeSubTypeTG.TREX:
581 raise ValueError(f"Unsupported TG subtype: {subtype!r}")
582 if u"trex-astf" in self.traffic_profile:
583 self.trex_astf_stop_remote_exec(self._node)
584 elif u"trex-stl" in self.traffic_profile:
585 self.trex_stl_stop_remote_exec(self._node)
587 raise ValueError(u"Unsupported T-Rex traffic profile!")
588 self._stop_time = time.monotonic()
590 return self._get_measurement_result()
592 def _compute_duration(self, duration, multiplier):
593 """Compute duration for profile driver.
595 The final result is influenced by transaction scale and duration limit.
596 It is assumed a higher level function has already set those to self.
597 The duration argument is the target value from search point of view,
598 before the overrides are applied here.
600 Minus one (signalling async traffic start) is kept.
602 Completeness flag is also included. Duration limited or async trials
603 are not considered complete for ramp-up purposes.
605 :param duration: Time expressed in seconds for how long to send traffic.
606 :param multiplier: Traffic rate in transactions per second.
607 :type duration: float
608 :type multiplier: float
609 :returns: New duration and whether it was a complete ramp-up candidate.
614 return duration, False
615 computed_duration = duration
616 if self.transaction_scale:
617 computed_duration = self.transaction_scale / multiplier
618 # Log the computed duration,
619 # so we can compare with what telemetry suggests
620 # the real duration was.
621 logger.debug(f"Expected duration {computed_duration}")
622 if not self.duration_limit:
623 return computed_duration, True
624 limited_duration = min(computed_duration, self.duration_limit)
625 return limited_duration, (limited_duration == computed_duration)
627 def trex_astf_start_remote_exec(
628 self, duration, multiplier, async_call=False):
629 """Execute T-Rex ASTF script on remote node over ssh to start running
632 In sync mode, measurement results are stored internally.
633 In async mode, initial data including xstats are stored internally.
635 This method contains the logic to compute duration as maximum time
636 if transaction_scale is nonzero.
637 The transaction_scale argument defines (limits) how many transactions
638 will be started in total. As that amount of transaction can take
639 considerable time (sometimes due to explicit delays in the profile),
640 the real time a trial needs to finish is computed here. For now,
641 in that case the duration argument is ignored, assuming it comes
642 from ASTF-unaware search algorithm. The overall time a single
643 transaction needs is given in parameter transaction_duration,
644 it includes both explicit delays and implicit time it takes
645 to transfer data (or whatever the transaction does).
647 Currently it is observed TRex does not start the ASTF traffic
648 immediately, an ad-hoc constant is added to the computed duration
649 to compensate for that.
651 If transaction_scale is zero, duration is not recomputed.
652 It is assumed the subsequent result parsing gets the real duration
653 if the traffic stops sooner for any reason.
655 Currently, it is assumed traffic profile defines a single transaction.
656 To avoid heavy logic here, the input rate is expected to be in
657 transactions per second, as that directly translates to TRex multiplier,
658 (assuming the profile does not override the default cps value of one).
660 :param duration: Time expressed in seconds for how long to send traffic.
661 :param multiplier: Traffic rate in transactions per second.
662 :param async_call: If enabled then don't wait for all incoming traffic.
663 :type duration: float
664 :type multiplier: int
665 :type async_call: bool
666 :raises RuntimeError: In case of T-Rex driver issue.
668 self.check_mode(TrexMode.ASTF)
669 p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
670 if not isinstance(duration, (float, int)):
671 duration = float(duration)
673 # TODO: Refactor the code so duration is computed only once,
674 # and both the initial and the computed durations are logged.
675 computed_duration, _ = self._compute_duration(duration, multiplier)
677 command_line = OptionString().add(u"python3")
678 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
679 command_line.add(f"'{dirname}/trex_astf_profile.py'")
680 command_line.change_prefix(u"--")
681 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
682 command_line.add_with_value(
683 u"profile", f"'{dirname}/{self.traffic_profile}.py'"
685 command_line.add_with_value(u"duration", f"{computed_duration!r}")
686 command_line.add_with_value(u"frame_size", self.frame_size)
687 command_line.add_with_value(
688 u"n_data_frames", Constants.ASTF_N_DATA_FRAMES
690 command_line.add_with_value(u"multiplier", multiplier)
691 command_line.add_with_value(u"port_0", p_0)
692 command_line.add_with_value(u"port_1", p_1)
693 command_line.add_with_value(
694 u"traffic_directions", self.traffic_directions
696 command_line.add_if(u"async_start", async_call)
697 command_line.add_if(u"latency", self.use_latency)
698 command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
699 command_line.add_with_value(
700 u"delay", Constants.PERF_TRIAL_ASTF_DELAY
703 self._start_time = time.monotonic()
704 self._rate = multiplier
705 stdout, _ = exec_cmd_no_error(
706 self._node, command_line, timeout=computed_duration + 10.0,
707 message=u"T-Rex ASTF runtime error!"
712 self._target_duration = None
713 self._duration = None
714 self._received = None
718 xstats = [None, None]
719 self._l7_data = dict()
720 self._l7_data[u"client"] = dict()
721 self._l7_data[u"client"][u"active_flows"] = None
722 self._l7_data[u"client"][u"established_flows"] = None
723 self._l7_data[u"client"][u"traffic_duration"] = None
724 self._l7_data[u"server"] = dict()
725 self._l7_data[u"server"][u"active_flows"] = None
726 self._l7_data[u"server"][u"established_flows"] = None
727 self._l7_data[u"server"][u"traffic_duration"] = None
728 if u"udp" in self.traffic_profile:
729 self._l7_data[u"client"][u"udp"] = dict()
730 self._l7_data[u"client"][u"udp"][u"connects"] = None
731 self._l7_data[u"client"][u"udp"][u"closed_flows"] = None
732 self._l7_data[u"client"][u"udp"][u"err_cwf"] = None
733 self._l7_data[u"server"][u"udp"] = dict()
734 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = None
735 self._l7_data[u"server"][u"udp"][u"closed_flows"] = None
736 elif u"tcp" in self.traffic_profile:
737 self._l7_data[u"client"][u"tcp"] = dict()
738 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = None
739 self._l7_data[u"client"][u"tcp"][u"connects"] = None
740 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = None
741 self._l7_data[u"client"][u"tcp"][u"connattempt"] = None
742 self._l7_data[u"server"][u"tcp"] = dict()
743 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = None
744 self._l7_data[u"server"][u"tcp"][u"connects"] = None
745 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = None
747 logger.warn(u"Unsupported T-Rex ASTF traffic profile!")
749 for line in stdout.splitlines():
750 if f"Xstats snapshot {index}: " in line:
751 xstats[index] = line[19:]
755 self._xstats = tuple(xstats)
757 self._target_duration = duration
758 self._duration = computed_duration
759 self._parse_traffic_results(stdout)
761 def trex_stl_start_remote_exec(self, duration, rate, async_call=False):
762 """Execute T-Rex STL script on remote node over ssh to start running
765 In sync mode, measurement results are stored internally.
766 In async mode, initial data including xstats are stored internally.
768 Mode-unaware code (e.g. in search algorithms) works with transactions.
769 To keep the logic simple, multiplier is set to that value.
770 As bidirectional traffic profiles send packets in both directions,
771 they are treated as transactions with two packets (one per direction).
773 :param duration: Time expressed in seconds for how long to send traffic.
774 :param rate: Traffic rate in transactions per second.
775 :param async_call: If enabled then don't wait for all incoming traffic.
776 :type duration: float
778 :type async_call: bool
779 :raises RuntimeError: In case of T-Rex driver issue.
781 self.check_mode(TrexMode.STL)
782 p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
783 if not isinstance(duration, (float, int)):
784 duration = float(duration)
786 # TODO: Refactor the code so duration is computed only once,
787 # and both the initial and the computed durations are logged.
788 duration, _ = self._compute_duration(duration=duration, multiplier=rate)
790 command_line = OptionString().add(u"python3")
791 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
792 command_line.add(f"'{dirname}/trex_stl_profile.py'")
793 command_line.change_prefix(u"--")
794 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
795 command_line.add_with_value(
796 u"profile", f"'{dirname}/{self.traffic_profile}.py'"
798 command_line.add_with_value(u"duration", f"{duration!r}")
799 command_line.add_with_value(u"frame_size", self.frame_size)
800 command_line.add_with_value(u"rate", f"{rate!r}")
801 command_line.add_with_value(u"port_0", p_0)
802 command_line.add_with_value(u"port_1", p_1)
803 command_line.add_with_value(
804 u"traffic_directions", self.traffic_directions
806 command_line.add_if(u"async_start", async_call)
807 command_line.add_if(u"latency", self.use_latency)
808 command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
809 command_line.add_with_value(u"delay", Constants.PERF_TRIAL_STL_DELAY)
811 # TODO: This is ugly. Handle parsing better.
812 self._start_time = time.monotonic()
813 self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
814 stdout, _ = exec_cmd_no_error(
815 self._node, command_line, timeout=int(duration) + 60,
816 message=u"T-Rex STL runtime error"
821 self._target_duration = None
822 self._duration = None
823 self._received = None
828 xstats = [None, None]
830 for line in stdout.splitlines():
831 if f"Xstats snapshot {index}: " in line:
832 xstats[index] = line[19:]
836 self._xstats = tuple(xstats)
838 self._target_duration = duration
839 self._duration = duration
840 self._parse_traffic_results(stdout)
842 def send_traffic_on_tg(
850 traffic_directions=2,
851 transaction_duration=0.0,
853 transaction_type=u"packet",
857 ramp_up_duration=None,
861 """Send traffic from all configured interfaces on TG.
863 In async mode, xstats is stored internally,
864 to enable getting correct result when stopping the traffic.
865 In both modes, stdout is returned,
866 but _parse_traffic_results only works in sync output.
868 Note that traffic generator uses DPDK driver which might
869 reorder port numbers based on wiring and PCI numbering.
870 This method handles that, so argument values are invariant,
871 but you can see swapped valued in debug logs.
873 When transaction_scale is specified, the duration value is ignored
874 and the needed time is computed. For cases where this results in
875 to too long measurement (e.g. teardown trial with small rate),
876 duration_limit is applied (of non-zero), so the trial is stopped sooner.
878 Bidirectional STL profiles are treated as transactions with two packets.
880 The return value is None for async.
882 :param duration: Duration of test traffic generation in seconds.
883 :param rate: Traffic rate in transactions per second.
884 :param frame_size: Frame size (L2) in Bytes.
885 :param traffic_profile: Module name as a traffic profile identifier.
886 See GPL/traffic_profiles/trex for implemented modules.
887 :param async_call: Async mode.
888 :param ppta: Packets per transaction, aggregated over directions.
889 Needed for udp_pps which does not have a good transaction counter,
890 so we need to compute expected number of packets.
892 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
894 :param transaction_duration: Total expected time to close transaction.
895 :param transaction_scale: Number of transactions to perform.
896 0 (default) means unlimited.
897 :param transaction_type: An identifier specifying which counters
898 and formulas to use when computing attempted and failed
899 transactions. Default: "packet".
900 :param duration_limit: Zero or maximum limit for computed (or given)
902 :param use_latency: Whether to measure latency during the trial.
904 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
905 :param ramp_up_duration: Duration of ramp-up trials [s].
906 :param state_timeout: Time of life of DUT state [s].
907 :param ramp_up_only: If true, do not perform main trial measurement.
908 :type duration: float
910 :type frame_size: str
911 :type traffic_profile: str
912 :type async_call: bool
914 :type traffic_directions: int
915 :type transaction_duration: float
916 :type transaction_scale: int
917 :type transaction_type: str
918 :type duration_limit: float
919 :type use_latency: bool
920 :type ramp_up_rate: float
921 :type ramp_up_duration: float
922 :type state_timeout: float
923 :type ramp_up_only: bool
924 :returns: TG results.
925 :rtype: ReceiveRateMeasurement or None
926 :raises ValueError: If TG traffic profile is not supported.
928 self.set_rate_provider_defaults(
929 frame_size=frame_size,
930 traffic_profile=traffic_profile,
932 traffic_directions=traffic_directions,
933 transaction_duration=transaction_duration,
934 transaction_scale=transaction_scale,
935 transaction_type=transaction_type,
936 duration_limit=duration_limit,
937 use_latency=use_latency,
938 ramp_up_rate=ramp_up_rate,
939 ramp_up_duration=ramp_up_duration,
940 state_timeout=state_timeout,
942 return self._send_traffic_on_tg_with_ramp_up(
945 async_call=async_call,
946 ramp_up_only=ramp_up_only,
949 def _send_traffic_on_tg_internal(
950 self, duration, rate, async_call=False):
951 """Send traffic from all configured interfaces on TG.
953 This is an internal function, it assumes set_rate_provider_defaults
954 has been called to remember most values.
955 The reason why need to remember various values is that
956 the traffic can be asynchronous, and parsing needs those values.
957 The reason why this is is a separate function from the one
958 which calls set_rate_provider_defaults is that some search algorithms
959 need to specify their own values, and we do not want the measure call
960 to overwrite them with defaults.
962 This function is used both for automated ramp-up trials
963 and for explicitly called trials.
965 :param duration: Duration of test traffic generation in seconds.
966 :param rate: Traffic rate in transactions per second.
967 :param async_call: Async mode.
968 :type duration: float
970 :type async_call: bool
971 :returns: TG results.
972 :rtype: ReceiveRateMeasurement or None
973 :raises ValueError: If TG traffic profile is not supported.
975 subtype = check_subtype(self._node)
976 if subtype == NodeSubTypeTG.TREX:
977 if u"trex-astf" in self.traffic_profile:
978 self.trex_astf_start_remote_exec(
979 duration, float(rate), async_call
981 elif u"trex-stl" in self.traffic_profile:
982 unit_rate_str = str(rate) + u"pps"
983 # TODO: Suport transaction_scale et al?
984 self.trex_stl_start_remote_exec(
985 duration, unit_rate_str, async_call
988 raise ValueError(u"Unsupported T-Rex traffic profile!")
990 return None if async_call else self._get_measurement_result()
992 def _send_traffic_on_tg_with_ramp_up(
993 self, duration, rate, async_call=False, ramp_up_only=False):
994 """Send traffic from all interfaces on TG, maybe after ramp-up.
996 This is an internal function, it assumes set_rate_provider_defaults
997 has been called to remember most values.
998 The reason why need to remember various values is that
999 the traffic can be asynchronous, and parsing needs those values.
1000 The reason why this is a separate function from the one
1001 which calls set_rate_provider_defaults is that some search algorithms
1002 need to specify their own values, and we do not want the measure call
1003 to overwrite them with defaults.
1005 If ramp-up tracking is detected, a computation is performed,
1006 and if state timeout is near, trial at ramp-up rate and duration
1007 is inserted before the main trial measurement.
1009 The ramp_up_only parameter forces a ramp-up without immediate
1010 trial measurement, which is useful in case self remembers
1011 a previous ramp-up trial that belongs to a different test (phase).
1013 Return None if trial is async or ramp-up only.
1015 :param duration: Duration of test traffic generation in seconds.
1016 :param rate: Traffic rate in transactions per second.
1017 :param async_call: Async mode.
1018 :param ramp_up_only: If true, do not perform main trial measurement.
1019 :type duration: float
1021 :type async_call: bool
1022 :type ramp_up_only: bool
1023 :returns: TG results.
1024 :rtype: ReceiveRateMeasurement or None
1025 :raises ValueError: If TG traffic profile is not supported.
1028 if self.ramp_up_rate:
1029 # Figure out whether we need to insert a ramp-up trial.
1030 # TODO: Give up on async_call=True?
1031 if ramp_up_only or self.ramp_up_start is None:
1032 # We never ramped up yet (at least not in this test case).
1033 ramp_up_needed = True
1035 # We ramped up before, but maybe it was too long ago.
1036 # Adding a constant overhead to be safe.
1037 time_now = time.monotonic() + 1.0
1038 computed_duration, complete = self._compute_duration(
1042 # There are two conditions for inserting ramp-up.
1043 # If early sessions are expiring already,
1044 # or if late sessions are to expire before measurement is over.
1045 ramp_up_start_delay = time_now - self.ramp_up_start
1046 ramp_up_stop_delay = time_now - self.ramp_up_stop
1047 ramp_up_stop_delay += computed_duration
1048 bigger_delay = max(ramp_up_start_delay, ramp_up_stop_delay)
1049 # Final boolean decision.
1050 ramp_up_needed = (bigger_delay >= self.state_timeout)
1053 u"State may time out during next real trial, "
1054 u"inserting a ramp-up trial."
1056 self.ramp_up_start = time.monotonic()
1057 self._send_traffic_on_tg_internal(
1058 duration=self.ramp_up_duration,
1059 rate=self.ramp_up_rate,
1060 async_call=async_call,
1062 self.ramp_up_stop = time.monotonic()
1063 logger.debug(u"Ramp-up done.")
1066 u"State will probably not time out during next real trial, "
1067 u"no ramp-up trial needed just yet."
1071 trial_start = time.monotonic()
1072 result = self._send_traffic_on_tg_internal(
1075 async_call=async_call,
1077 trial_end = time.monotonic()
1078 if self.ramp_up_rate:
1079 # Optimization: No loss acts as a good ramp-up, if it was complete.
1080 if complete and result is not None and result.loss_count == 0:
1081 logger.debug(u"Good trial acts as a ramp-up")
1082 self.ramp_up_start = trial_start
1083 self.ramp_up_stop = trial_end
1085 logger.debug(u"Loss or incomplete, does not act as a ramp-up.")
1088 def no_traffic_loss_occurred(self):
1089 """Fail if loss occurred in traffic run.
1092 :raises Exception: If loss occured.
1094 if self._loss is None:
1095 raise RuntimeError(u"The traffic generation has not been issued")
1096 if self._loss != u"0":
1097 raise RuntimeError(f"Traffic loss occurred: {self._loss}")
1099 def fail_if_no_traffic_forwarded(self):
1100 """Fail if no traffic forwarded.
1102 TODO: Check number of passed transactions instead.
1105 :raises Exception: If no traffic forwarded.
1107 if self._received is None:
1108 raise RuntimeError(u"The traffic generation has not been issued")
1109 if self._received == 0:
1110 raise RuntimeError(u"No traffic forwarded")
1112 def partial_traffic_loss_accepted(
1113 self, loss_acceptance, loss_acceptance_type):
1114 """Fail if loss is higher then accepted in traffic run.
1116 :param loss_acceptance: Permitted drop ratio or frames count.
1117 :param loss_acceptance_type: Type of permitted loss.
1118 :type loss_acceptance: float
1119 :type loss_acceptance_type: LossAcceptanceType
1121 :raises Exception: If loss is above acceptance criteria.
1123 if self._loss is None:
1124 raise Exception(u"The traffic generation has not been issued")
1126 if loss_acceptance_type == u"percentage":
1127 loss = (float(self._loss) / float(self._sent)) * 100
1128 elif loss_acceptance_type == u"frames":
1129 loss = float(self._loss)
1131 raise Exception(u"Loss acceptance type not supported")
1133 if loss > float(loss_acceptance):
1135 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
1138 def _parse_traffic_results(self, stdout):
1139 """Parse stdout of scripts into fields of self.
1141 Block of code to reuse, by sync start, or stop after async.
1143 :param stdout: Text containing the standard output.
1146 subtype = check_subtype(self._node)
1147 if subtype == NodeSubTypeTG.TREX:
1148 # Last line from console output
1149 line = stdout.splitlines()[-1]
1150 results = line.split(u";")
1151 if results[-1] in (u" ", u""):
1153 self._result = dict()
1154 for result in results:
1155 key, value = result.split(u"=", maxsplit=1)
1156 self._result[key.strip()] = value
1157 logger.info(f"TrafficGen results:\n{self._result}")
1158 self._received = int(self._result.get(u"total_received"), 0)
1159 self._sent = int(self._result.get(u"total_sent", 0))
1160 self._loss = int(self._result.get(u"frame_loss", 0))
1161 self._approximated_duration = \
1162 self._result.get(u"approximated_duration", 0.0)
1163 if u"manual" not in str(self._approximated_duration):
1164 self._approximated_duration = float(self._approximated_duration)
1165 self._latency = list()
1166 self._latency.append(self._result.get(u"latency_stream_0(usec)"))
1167 self._latency.append(self._result.get(u"latency_stream_1(usec)"))
1168 if self._mode == TrexMode.ASTF:
1169 self._l7_data = dict()
1170 self._l7_data[u"client"] = dict()
1171 self._l7_data[u"client"][u"sent"] = \
1172 int(self._result.get(u"client_sent", 0))
1173 self._l7_data[u"client"][u"received"] = \
1174 int(self._result.get(u"client_received", 0))
1175 self._l7_data[u"client"][u"active_flows"] = \
1176 int(self._result.get(u"client_active_flows", 0))
1177 self._l7_data[u"client"][u"established_flows"] = \
1178 int(self._result.get(u"client_established_flows", 0))
1179 self._l7_data[u"client"][u"traffic_duration"] = \
1180 float(self._result.get(u"client_traffic_duration", 0.0))
1181 self._l7_data[u"client"][u"err_rx_throttled"] = \
1182 int(self._result.get(u"client_err_rx_throttled", 0))
1183 self._l7_data[u"client"][u"err_c_nf_throttled"] = \
1184 int(self._result.get(u"client_err_nf_throttled", 0))
1185 self._l7_data[u"client"][u"err_flow_overflow"] = \
1186 int(self._result.get(u"client_err_flow_overflow", 0))
1187 self._l7_data[u"server"] = dict()
1188 self._l7_data[u"server"][u"active_flows"] = \
1189 int(self._result.get(u"server_active_flows", 0))
1190 self._l7_data[u"server"][u"established_flows"] = \
1191 int(self._result.get(u"server_established_flows", 0))
1192 self._l7_data[u"server"][u"traffic_duration"] = \
1193 float(self._result.get(u"server_traffic_duration", 0.0))
1194 self._l7_data[u"server"][u"err_rx_throttled"] = \
1195 int(self._result.get(u"client_err_rx_throttled", 0))
1196 if u"udp" in self.traffic_profile:
1197 self._l7_data[u"client"][u"udp"] = dict()
1198 self._l7_data[u"client"][u"udp"][u"connects"] = \
1199 int(self._result.get(u"client_udp_connects", 0))
1200 self._l7_data[u"client"][u"udp"][u"closed_flows"] = \
1201 int(self._result.get(u"client_udp_closed", 0))
1202 self._l7_data[u"client"][u"udp"][u"tx_bytes"] = \
1203 int(self._result.get(u"client_udp_tx_bytes", 0))
1204 self._l7_data[u"client"][u"udp"][u"rx_bytes"] = \
1205 int(self._result.get(u"client_udp_rx_bytes", 0))
1206 self._l7_data[u"client"][u"udp"][u"tx_packets"] = \
1207 int(self._result.get(u"client_udp_tx_packets", 0))
1208 self._l7_data[u"client"][u"udp"][u"rx_packets"] = \
1209 int(self._result.get(u"client_udp_rx_packets", 0))
1210 self._l7_data[u"client"][u"udp"][u"keep_drops"] = \
1211 int(self._result.get(u"client_udp_keep_drops", 0))
1212 self._l7_data[u"client"][u"udp"][u"err_cwf"] = \
1213 int(self._result.get(u"client_err_cwf", 0))
1214 self._l7_data[u"server"][u"udp"] = dict()
1215 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = \
1216 int(self._result.get(u"server_udp_accepts", 0))
1217 self._l7_data[u"server"][u"udp"][u"closed_flows"] = \
1218 int(self._result.get(u"server_udp_closed", 0))
1219 self._l7_data[u"server"][u"udp"][u"tx_bytes"] = \
1220 int(self._result.get(u"server_udp_tx_bytes", 0))
1221 self._l7_data[u"server"][u"udp"][u"rx_bytes"] = \
1222 int(self._result.get(u"server_udp_rx_bytes", 0))
1223 self._l7_data[u"server"][u"udp"][u"tx_packets"] = \
1224 int(self._result.get(u"server_udp_tx_packets", 0))
1225 self._l7_data[u"server"][u"udp"][u"rx_packets"] = \
1226 int(self._result.get(u"server_udp_rx_packets", 0))
1227 elif u"tcp" in self.traffic_profile:
1228 self._l7_data[u"client"][u"tcp"] = dict()
1229 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = \
1230 int(self._result.get(u"client_tcp_connect_inits", 0))
1231 self._l7_data[u"client"][u"tcp"][u"connects"] = \
1232 int(self._result.get(u"client_tcp_connects", 0))
1233 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = \
1234 int(self._result.get(u"client_tcp_closed", 0))
1235 self._l7_data[u"client"][u"tcp"][u"connattempt"] = \
1236 int(self._result.get(u"client_tcp_connattempt", 0))
1237 self._l7_data[u"client"][u"tcp"][u"tx_bytes"] = \
1238 int(self._result.get(u"client_tcp_tx_bytes", 0))
1239 self._l7_data[u"client"][u"tcp"][u"rx_bytes"] = \
1240 int(self._result.get(u"client_tcp_rx_bytes", 0))
1241 self._l7_data[u"server"][u"tcp"] = dict()
1242 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = \
1243 int(self._result.get(u"server_tcp_accepts", 0))
1244 self._l7_data[u"server"][u"tcp"][u"connects"] = \
1245 int(self._result.get(u"server_tcp_connects", 0))
1246 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = \
1247 int(self._result.get(u"server_tcp_closed", 0))
1248 self._l7_data[u"server"][u"tcp"][u"tx_bytes"] = \
1249 int(self._result.get(u"server_tcp_tx_bytes", 0))
1250 self._l7_data[u"server"][u"tcp"][u"rx_bytes"] = \
1251 int(self._result.get(u"server_tcp_rx_bytes", 0))
1253 def _get_measurement_result(self):
1254 """Return the result of last measurement as ReceiveRateMeasurement.
1256 Separate function, as measurements can end either by time
1257 or by explicit call, this is the common block at the end.
1259 The target_tr field of ReceiveRateMeasurement is in
1260 transactions per second. Transmit count and loss count units
1261 depend on the transaction type. Usually they are in transactions
1262 per second, or aggregated packets per second.
1264 TODO: Fail on running or already reported measurement.
1266 :returns: Structure containing the result of the measurement.
1267 :rtype: ReceiveRateMeasurement
1270 # Client duration seems to include a setup period
1271 # where TRex does not send any packets yet.
1272 # Server duration does not include it.
1273 server_data = self._l7_data[u"server"]
1274 approximated_duration = float(server_data[u"traffic_duration"])
1275 except (KeyError, AttributeError, ValueError, TypeError):
1276 approximated_duration = None
1278 if not approximated_duration:
1279 approximated_duration = float(self._approximated_duration)
1280 except ValueError: # "manual"
1281 approximated_duration = None
1282 if not approximated_duration:
1283 if self._duration and self._duration > 0:
1284 # Known recomputed or target duration.
1285 approximated_duration = self._duration
1287 # It was an explicit stop.
1288 if not self._stop_time:
1289 raise RuntimeError(u"Unable to determine duration.")
1290 approximated_duration = self._stop_time - self._start_time
1291 target_duration = self._target_duration
1292 if not target_duration:
1293 target_duration = approximated_duration
1294 transmit_rate = self._rate
1296 if self.transaction_type == u"packet":
1297 partial_attempt_count = self._sent
1298 packet_rate = transmit_rate * self.ppta
1299 # We have a float. TRex way of rounding it is not obvious.
1300 # The biggest source of mismatch is Inter Stream Gap.
1301 # So the code tolerates 10 usec of missing packets.
1302 expected_attempt_count = (target_duration - 1e-5) * packet_rate
1303 expected_attempt_count = math.ceil(expected_attempt_count)
1304 # TRex can send more.
1305 expected_attempt_count = max(expected_attempt_count, self._sent)
1306 unsent = expected_attempt_count - self._sent
1307 pass_count = self._received
1308 fail_count = expected_attempt_count - pass_count
1309 elif self.transaction_type == u"udp_cps":
1310 if not self.transaction_scale:
1311 raise RuntimeError(u"Add support for no-limit udp_cps.")
1312 partial_attempt_count = self._l7_data[u"client"][u"sent"]
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 pass_count = self._l7_data[u"client"][u"received"]
1317 fail_count = expected_attempt_count - pass_count
1318 elif self.transaction_type == u"tcp_cps":
1319 if not self.transaction_scale:
1320 raise RuntimeError(u"Add support for no-limit tcp_cps.")
1321 ctca = self._l7_data[u"client"][u"tcp"][u"connattempt"]
1322 partial_attempt_count = ctca
1323 # We do not care whether TG is slow, it should have attempted all.
1324 expected_attempt_count = self.transaction_scale
1325 unsent = expected_attempt_count - partial_attempt_count
1326 # From TCP point of view, server/connects counts full connections,
1327 # but we are testing NAT session so client/connects counts that
1328 # (half connections from TCP point of view).
1329 pass_count = self._l7_data[u"client"][u"tcp"][u"connects"]
1330 fail_count = expected_attempt_count - pass_count
1331 elif self.transaction_type == u"udp_pps":
1332 if not self.transaction_scale:
1333 raise RuntimeError(u"Add support for no-limit udp_pps.")
1334 partial_attempt_count = self._sent
1335 expected_attempt_count = self.transaction_scale * self.ppta
1336 unsent = expected_attempt_count - self._sent
1337 fail_count = self._loss + unsent
1338 elif self.transaction_type == u"tcp_pps":
1339 if not self.transaction_scale:
1340 raise RuntimeError(u"Add support for no-limit tcp_pps.")
1341 partial_attempt_count = self._sent
1342 expected_attempt_count = self.transaction_scale * self.ppta
1343 # One loss-like scenario happens when TRex receives all packets
1344 # on L2 level, but is not fast enough to process them all
1345 # at L7 level, which leads to retransmissions.
1346 # Those manifest as opackets larger than expected.
1347 # A simple workaround is to add absolute difference.
1348 # Probability of retransmissions exactly cancelling
1349 # packets unsent due to duration stretching is quite low.
1350 unsent = abs(expected_attempt_count - self._sent)
1351 fail_count = self._loss + unsent
1353 raise RuntimeError(f"Unknown parsing {self.transaction_type!r}")
1354 if unsent and isinstance(self._approximated_duration, float):
1355 # Do not report unsent for "manual".
1356 logger.debug(f"Unsent packets/transactions: {unsent}")
1357 if fail_count < 0 and not self.negative_loss:
1359 measurement = ReceiveRateMeasurement(
1360 duration=target_duration,
1361 target_tr=transmit_rate,
1362 transmit_count=expected_attempt_count,
1363 loss_count=fail_count,
1364 approximated_duration=approximated_duration,
1365 partial_transmit_count=partial_attempt_count,
1367 measurement.latency = self.get_latency_int()
1370 def measure(self, duration, transmit_rate):
1371 """Run trial measurement, parse and return results.
1373 The input rate is for transactions. Stateles bidirectional traffic
1374 is understood as sequence of (asynchronous) transactions,
1377 The result units depend on test type, generally
1378 the count either transactions or packets (aggregated over directions).
1380 Optionally, this method sleeps if measurement finished before
1381 the time specified as duration.
1383 :param duration: Trial duration [s].
1384 :param transmit_rate: Target rate in transactions per second.
1385 :type duration: float
1386 :type transmit_rate: float
1387 :returns: Structure containing the result of the measurement.
1388 :rtype: ReceiveRateMeasurement
1389 :raises RuntimeError: If TG is not set or if node is not TG
1390 or if subtype is not specified.
1391 :raises NotImplementedError: If TG is not supported.
1393 duration = float(duration)
1394 time_start = time.monotonic()
1395 time_stop = time_start + duration
1398 result = self._send_traffic_on_tg_with_ramp_up(
1403 logger.debug(f"trial measurement result: {result!r}")
1404 # In PLRsearch, computation needs the specified time to complete.
1405 if self.sleep_till_duration:
1406 sleeptime = time_stop - time.monotonic()
1408 # TODO: Sometimes we have time to do additional trials here,
1409 # adapt PLRsearch to accept all the results.
1410 time.sleep(sleeptime)
1413 def set_rate_provider_defaults(
1419 traffic_directions=2,
1420 transaction_duration=0.0,
1421 transaction_scale=0,
1422 transaction_type=u"packet",
1425 sleep_till_duration=False,
1428 ramp_up_duration=None,
1429 state_timeout=240.0,
1431 """Store values accessed by measure().
1433 :param frame_size: Frame size identifier or value [B].
1434 :param traffic_profile: Module name as a traffic profile identifier.
1435 See GPL/traffic_profiles/trex for implemented modules.
1436 :param ppta: Packets per transaction, aggregated over directions.
1437 Needed for udp_pps which does not have a good transaction counter,
1438 so we need to compute expected number of packets.
1440 :param resetter: Callable to reset DUT state for repeated trials.
1441 :param traffic_directions: Traffic from packet counting point of view
1442 is bi- (2) or uni- (1) directional.
1444 :param transaction_duration: Total expected time to close transaction.
1445 :param transaction_scale: Number of transactions to perform.
1446 0 (default) means unlimited.
1447 :param transaction_type: An identifier specifying which counters
1448 and formulas to use when computing attempted and failed
1449 transactions. Default: "packet".
1450 TODO: Does this also specify parsing for the measured duration?
1451 :param duration_limit: Zero or maximum limit for computed (or given)
1453 :param negative_loss: If false, negative loss is reported as zero loss.
1454 :param sleep_till_duration: If true and measurement returned faster,
1455 sleep until it matches duration. Needed for PLRsearch.
1456 :param use_latency: Whether to measure latency during the trial.
1458 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1459 :param ramp_up_duration: Duration of ramp-up trials [s].
1460 :param state_timeout: Time of life of DUT state [s].
1461 :type frame_size: str or int
1462 :type traffic_profile: str
1464 :type resetter: Optional[Callable[[], None]]
1465 :type traffic_directions: int
1466 :type transaction_duration: float
1467 :type transaction_scale: int
1468 :type transaction_type: str
1469 :type duration_limit: float
1470 :type negative_loss: bool
1471 :type sleep_till_duration: bool
1472 :type use_latency: bool
1473 :type ramp_up_rate: float
1474 :type ramp_up_duration: float
1475 :type state_timeout: float
1477 self.frame_size = frame_size
1478 self.traffic_profile = str(traffic_profile)
1479 self.resetter = resetter
1481 self.traffic_directions = int(traffic_directions)
1482 self.transaction_duration = float(transaction_duration)
1483 self.transaction_scale = int(transaction_scale)
1484 self.transaction_type = str(transaction_type)
1485 self.duration_limit = float(duration_limit)
1486 self.negative_loss = bool(negative_loss)
1487 self.sleep_till_duration = bool(sleep_till_duration)
1488 self.use_latency = bool(use_latency)
1489 self.ramp_up_rate = float(ramp_up_rate)
1490 self.ramp_up_duration = float(ramp_up_duration)
1491 self.state_timeout = float(state_timeout)
1494 class OptimizedSearch:
1495 """Class to be imported as Robot Library, containing search keywords.
1497 Aside of setting up measurer and forwarding arguments,
1498 the main business is to translate min/max rate from unidir to aggregated.
1502 def perform_optimized_ndrpdr_search(
1505 minimum_transmit_rate,
1506 maximum_transmit_rate,
1507 packet_loss_ratio=0.005,
1508 final_relative_width=0.005,
1509 final_trial_duration=30.0,
1510 initial_trial_duration=1.0,
1511 number_of_intermediate_phases=2,
1515 traffic_directions=2,
1516 transaction_duration=0.0,
1517 transaction_scale=0,
1518 transaction_type=u"packet",
1521 ramp_up_duration=None,
1522 state_timeout=240.0,
1523 expansion_coefficient=4.0,
1525 """Setup initialized TG, perform optimized search, return intervals.
1527 If transaction_scale is nonzero, all init and non-init trial durations
1528 are set to 1.0 (as they do not affect the real trial duration)
1529 and zero intermediate phases are used.
1530 This way no re-measurement happens.
1531 Warmup has to be handled via resetter or ramp-up mechanisms.
1533 :param frame_size: Frame size identifier or value [B].
1534 :param traffic_profile: Module name as a traffic profile identifier.
1535 See GPL/traffic_profiles/trex for implemented modules.
1536 :param minimum_transmit_rate: Minimal load in transactions per second.
1537 :param maximum_transmit_rate: Maximal load in transactions per second.
1538 :param packet_loss_ratio: Ratio of packets lost, for PDR [1].
1539 :param final_relative_width: Final lower bound transmit rate
1540 cannot be more distant that this multiple of upper bound [1].
1541 :param final_trial_duration: Trial duration for the final phase [s].
1542 :param initial_trial_duration: Trial duration for the initial phase
1543 and also for the first intermediate phase [s].
1544 :param number_of_intermediate_phases: Number of intermediate phases
1545 to perform before the final phase [1].
1546 :param timeout: The search will fail itself when not finished
1547 before this overall time [s].
1548 :param ppta: Packets per transaction, aggregated over directions.
1549 Needed for udp_pps which does not have a good transaction counter,
1550 so we need to compute expected number of packets.
1552 :param resetter: Callable to reset DUT state for repeated trials.
1553 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1555 :param transaction_duration: Total expected time to close transaction.
1556 :param transaction_scale: Number of transactions to perform.
1557 0 (default) means unlimited.
1558 :param transaction_type: An identifier specifying which counters
1559 and formulas to use when computing attempted and failed
1560 transactions. Default: "packet".
1561 :param use_latency: Whether to measure latency during the trial.
1563 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1564 :param ramp_up_duration: Duration of ramp-up trials [s].
1565 :param state_timeout: Time of life of DUT state [s].
1566 :param expansion_coefficient: In external search multiply width by this.
1567 :type frame_size: str or int
1568 :type traffic_profile: str
1569 :type minimum_transmit_rate: float
1570 :type maximum_transmit_rate: float
1571 :type packet_loss_ratio: float
1572 :type final_relative_width: float
1573 :type final_trial_duration: float
1574 :type initial_trial_duration: float
1575 :type number_of_intermediate_phases: int
1576 :type timeout: float
1578 :type resetter: Optional[Callable[[], None]]
1579 :type traffic_directions: int
1580 :type transaction_duration: float
1581 :type transaction_scale: int
1582 :type transaction_type: str
1583 :type use_latency: bool
1584 :type ramp_up_rate: float
1585 :type ramp_up_duration: float
1586 :type state_timeout: float
1587 :type expansion_coefficient: float
1588 :returns: Structure containing narrowed down NDR and PDR intervals
1589 and their measurements.
1590 :rtype: List[Receiverateinterval]
1591 :raises RuntimeError: If total duration is larger than timeout.
1593 # we need instance of TrafficGenerator instantiated by Robot Framework
1594 # to be able to use trex_stl-*()
1595 tg_instance = BuiltIn().get_library_instance(
1596 u"resources.libraries.python.TrafficGenerator"
1598 # Overrides for fixed transaction amount.
1599 # TODO: Move to robot code? We have two call sites, so this saves space,
1600 # even though this is surprising for log readers.
1601 if transaction_scale:
1602 initial_trial_duration = 1.0
1603 final_trial_duration = 1.0
1604 number_of_intermediate_phases = 0
1605 timeout += transaction_scale * 3e-4
1606 tg_instance.set_rate_provider_defaults(
1607 frame_size=frame_size,
1608 traffic_profile=traffic_profile,
1609 sleep_till_duration=False,
1612 traffic_directions=traffic_directions,
1613 transaction_duration=transaction_duration,
1614 transaction_scale=transaction_scale,
1615 transaction_type=transaction_type,
1616 use_latency=use_latency,
1617 ramp_up_rate=ramp_up_rate,
1618 ramp_up_duration=ramp_up_duration,
1619 state_timeout=state_timeout,
1621 algorithm = MultipleLossRatioSearch(
1622 measurer=tg_instance,
1623 final_trial_duration=final_trial_duration,
1624 final_relative_width=final_relative_width,
1625 number_of_intermediate_phases=number_of_intermediate_phases,
1626 initial_trial_duration=initial_trial_duration,
1629 expansion_coefficient=expansion_coefficient,
1631 if packet_loss_ratio:
1632 packet_loss_ratios = [0.0, packet_loss_ratio]
1634 # Happens in reconf tests.
1635 packet_loss_ratios = [packet_loss_ratio]
1636 results = algorithm.narrow_down_intervals(
1637 min_rate=minimum_transmit_rate,
1638 max_rate=maximum_transmit_rate,
1639 packet_loss_ratios=packet_loss_ratios,
1644 def perform_soak_search(
1647 minimum_transmit_rate,
1648 maximum_transmit_rate,
1655 trace_enabled=False,
1656 traffic_directions=2,
1657 transaction_duration=0.0,
1658 transaction_scale=0,
1659 transaction_type=u"packet",
1662 ramp_up_duration=None,
1663 state_timeout=240.0,
1665 """Setup initialized TG, perform soak search, return avg and stdev.
1667 :param frame_size: Frame size identifier or value [B].
1668 :param traffic_profile: Module name as a traffic profile identifier.
1669 See GPL/traffic_profiles/trex for implemented modules.
1670 :param minimum_transmit_rate: Minimal load in transactions per second.
1671 :param maximum_transmit_rate: Maximal load in transactions per second.
1672 :param plr_target: Ratio of packets lost to achieve [1].
1673 :param tdpt: Trial duration per trial.
1674 The algorithm linearly increases trial duration with trial number,
1675 this is the increment between succesive trials, in seconds.
1676 :param initial_count: Offset to apply before the first trial.
1677 For example initial_count=50 makes first trial to be 51*tdpt long.
1678 This is needed because initial "search" phase of integrator
1679 takes significant time even without any trial results.
1680 :param timeout: The search will stop after this overall time [s].
1681 :param ppta: Packets per transaction, aggregated over directions.
1682 Needed for udp_pps which does not have a good transaction counter,
1683 so we need to compute expected number of packets.
1685 :param resetter: Callable to reset DUT state for repeated trials.
1686 :param trace_enabled: True if trace enabled else False.
1687 This is very verbose tracing on numeric computations,
1688 do not use in production.
1690 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1692 :param transaction_duration: Total expected time to close transaction.
1693 :param transaction_scale: Number of transactions to perform.
1694 0 (default) means unlimited.
1695 :param transaction_type: An identifier specifying which counters
1696 and formulas to use when computing attempted and failed
1697 transactions. Default: "packet".
1698 :param use_latency: Whether to measure latency during the trial.
1700 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1701 :param ramp_up_duration: Duration of ramp-up trials [s].
1702 :param state_timeout: Time of life of DUT state [s].
1703 :type frame_size: str or int
1704 :type traffic_profile: str
1705 :type minimum_transmit_rate: float
1706 :type maximum_transmit_rate: float
1707 :type plr_target: float
1708 :type initial_count: int
1709 :type timeout: float
1711 :type resetter: Optional[Callable[[], None]]
1712 :type trace_enabled: bool
1713 :type traffic_directions: int
1714 :type transaction_duration: float
1715 :type transaction_scale: int
1716 :type transaction_type: str
1717 :type use_latency: bool
1718 :type ramp_up_rate: float
1719 :type ramp_up_duration: float
1720 :type state_timeout: float
1721 :returns: Average and stdev of estimated aggregated rate giving PLR.
1722 :rtype: 2-tuple of float
1724 tg_instance = BuiltIn().get_library_instance(
1725 u"resources.libraries.python.TrafficGenerator"
1727 # Overrides for fixed transaction amount.
1728 # TODO: Move to robot code? We have a single call site
1729 # but MLRsearch has two and we want the two to be used similarly.
1730 if transaction_scale:
1731 # TODO: What is a good value for max scale?
1732 # TODO: Scale the timeout with transaction scale.
1734 tg_instance.set_rate_provider_defaults(
1735 frame_size=frame_size,
1736 traffic_profile=traffic_profile,
1737 negative_loss=False,
1738 sleep_till_duration=True,
1741 traffic_directions=traffic_directions,
1742 transaction_duration=transaction_duration,
1743 transaction_scale=transaction_scale,
1744 transaction_type=transaction_type,
1745 use_latency=use_latency,
1746 ramp_up_rate=ramp_up_rate,
1747 ramp_up_duration=ramp_up_duration,
1748 state_timeout=state_timeout,
1750 algorithm = PLRsearch(
1751 measurer=tg_instance,
1752 trial_duration_per_trial=tdpt,
1753 packet_loss_ratio_target=plr_target,
1754 trial_number_offset=initial_count,
1756 trace_enabled=trace_enabled,
1758 result = algorithm.search(
1759 min_rate=minimum_transmit_rate,
1760 max_rate=maximum_transmit_rate,