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!"
255 # TODO: pylint says disable=too-many-locals.
256 def initialize_traffic_generator(
257 self, tg_node, tg_if1, tg_if2, tg_if1_adj_node, tg_if1_adj_if,
258 tg_if2_adj_node, tg_if2_adj_if, osi_layer, tg_if1_dst_mac=None,
259 tg_if2_dst_mac=None):
260 """TG initialization.
262 TODO: Document why do we need (and how do we use) _ifaces_reordered.
264 :param tg_node: Traffic generator node.
265 :param tg_if1: TG - name of first interface.
266 :param tg_if2: TG - name of second interface.
267 :param tg_if1_adj_node: TG if1 adjecent node.
268 :param tg_if1_adj_if: TG if1 adjecent interface.
269 :param tg_if2_adj_node: TG if2 adjecent node.
270 :param tg_if2_adj_if: TG if2 adjecent interface.
271 :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
272 :param tg_if1_dst_mac: Interface 1 destination MAC address.
273 :param tg_if2_dst_mac: Interface 2 destination MAC address.
277 :type tg_if1_adj_node: dict
278 :type tg_if1_adj_if: str
279 :type tg_if2_adj_node: dict
280 :type tg_if2_adj_if: str
282 :type tg_if1_dst_mac: str
283 :type tg_if2_dst_mac: str
285 :raises RuntimeError: In case of issue during initialization.
287 subtype = check_subtype(tg_node)
288 if subtype == NodeSubTypeTG.TREX:
290 self._mode = TrexMode.ASTF if osi_layer == u"L7" else TrexMode.STL
293 if1[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if1)
294 if2[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if2)
295 if1[u"addr"] = Topology().get_interface_mac(self._node, tg_if1)
296 if2[u"addr"] = Topology().get_interface_mac(self._node, tg_if2)
298 if osi_layer == u"L2":
299 if1[u"adj_addr"] = if2[u"addr"]
300 if2[u"adj_addr"] = if1[u"addr"]
301 elif osi_layer in (u"L3", u"L7"):
302 if1[u"adj_addr"] = Topology().get_interface_mac(
303 tg_if1_adj_node, tg_if1_adj_if
305 if2[u"adj_addr"] = Topology().get_interface_mac(
306 tg_if2_adj_node, tg_if2_adj_if
309 raise ValueError(u"Unknown OSI layer!")
311 # in case of switched environment we can override MAC addresses
312 if tg_if1_dst_mac is not None and tg_if2_dst_mac is not None:
313 if1[u"adj_addr"] = tg_if1_dst_mac
314 if2[u"adj_addr"] = tg_if2_dst_mac
316 if min(if1[u"pci"], if2[u"pci"]) != if1[u"pci"]:
318 self._ifaces_reordered = True
320 master_thread_id, latency_thread_id, socket, threads = \
321 CpuUtils.get_affinity_trex(
322 self._node, tg_if1, tg_if2,
323 tg_dtc=Constants.TREX_CORE_COUNT)
325 if osi_layer in (u"L2", u"L3", u"L7"):
328 f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
330 f" c: {len(threads)}\n"
331 f" limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
332 f" interfaces: [\"{if1[u'pci']}\",\"{if2[u'pci']}\"]\n"
334 f" - dest_mac: \'{if1[u'adj_addr']}\'\n"
335 f" src_mac: \'{if1[u'addr']}\'\n"
336 f" - dest_mac: \'{if2[u'adj_addr']}\'\n"
337 f" src_mac: \'{if2[u'addr']}\'\n"
339 f" master_thread_id: {master_thread_id}\n"
340 f" latency_thread_id: {latency_thread_id}\n"
342 f" - socket: {socket}\n"
343 f" threads: {threads}\n"
345 sudo=True, message=u"T-Rex config generation!"
348 if Constants.TREX_RX_DESCRIPTORS_COUNT != 0:
351 f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
352 f" rx_desc: {Constants.TREX_RX_DESCRIPTORS_COUNT}\n"
354 sudo=True, message=u"T-Rex rx_desc modification!"
357 if Constants.TREX_TX_DESCRIPTORS_COUNT != 0:
360 f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
361 f" tx_desc: {Constants.TREX_TX_DESCRIPTORS_COUNT}\n"
363 sudo=True, message=u"T-Rex tx_desc modification!"
366 raise ValueError(u"Unknown OSI layer!")
368 TrafficGenerator.startup_trex(
369 self._node, osi_layer, subtype=subtype
373 def startup_trex(tg_node, osi_layer, subtype=None):
374 """Startup sequence for the TRex traffic generator.
376 :param tg_node: Traffic generator node.
377 :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
378 :param subtype: Traffic generator sub-type.
381 :type subtype: NodeSubTypeTG
382 :raises RuntimeError: If T-Rex startup failed.
383 :raises ValueError: If OSI layer is not supported.
386 subtype = check_subtype(tg_node)
387 if subtype == NodeSubTypeTG.TREX:
388 for _ in range(0, 3):
389 # Kill TRex only if it is already running.
390 cmd = u"sh -c \"pgrep t-rex && pkill t-rex && sleep 3 || true\""
392 tg_node, cmd, sudo=True, message=u"Kill TRex failed!"
397 for port in tg_node[u"interfaces"].values():
398 if u'Mellanox' not in port.get(u'model'):
399 ports += f" {port.get(u'pci_address')}"
401 cmd = f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && " \
402 f"./dpdk_nic_bind.py -u {ports} || true\""
404 tg_node, cmd, sudo=True,
405 message=u"Unbind PCI ports from driver failed!"
409 cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
410 trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
412 trex_cmd.add(u"--prefix $(hostname)")
413 trex_cmd.add(u"--hdrh")
414 trex_cmd.add(u"--no-scapy-server")
415 trex_cmd.add_if(u"--astf", osi_layer == u"L7")
416 # OptionString does not create double space if extra is empty.
417 trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
418 inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
419 cmd = f"sh -c \"{inner_command}\" > /dev/null"
421 exec_cmd_no_error(tg_node, cmd, sudo=True)
423 cmd = u"sh -c \"cat /tmp/trex.log\""
425 tg_node, cmd, sudo=True,
426 message=u"Get TRex logs failed!"
428 raise RuntimeError(u"Start TRex failed!")
430 # Test T-Rex API responsiveness.
431 cmd = f"python3 {Constants.REMOTE_FW_DIR}/GPL/tools/trex/"
432 if osi_layer in (u"L2", u"L3"):
433 cmd += u"trex_stl_assert.py"
434 elif osi_layer == u"L7":
435 cmd += u"trex_astf_assert.py"
437 raise ValueError(u"Unknown OSI layer!")
440 tg_node, cmd, sudo=True,
441 message=u"T-Rex API is not responding!", retries=20
446 # After max retries TRex is still not responding to API critical
448 exec_cmd(tg_node, u"cat /tmp/trex.log", sudo=True)
449 raise RuntimeError(u"Start T-Rex failed after multiple retries!")
452 def is_trex_running(node):
453 """Check if T-Rex is running using pidof.
455 :param node: Traffic generator node.
457 :returns: True if T-Rex is running otherwise False.
460 ret, _, _ = exec_cmd(node, u"pgrep t-rex", sudo=True)
461 return bool(int(ret) == 0)
464 def teardown_traffic_generator(node):
467 :param node: Traffic generator node.
470 :raises RuntimeError: If node type is not a TG,
471 or if T-Rex teardown fails.
473 subtype = check_subtype(node)
474 if subtype == NodeSubTypeTG.TREX:
478 u"\"if pgrep t-rex; then sudo pkill t-rex && sleep 3; fi\"",
480 message=u"T-Rex kill failed!"
483 def trex_astf_stop_remote_exec(self, node):
484 """Execute T-Rex ASTF script on remote node over ssh to stop running
487 Internal state is updated with measurement results.
489 :param node: T-Rex generator node.
491 :raises RuntimeError: If stop traffic script fails.
493 command_line = OptionString().add(u"python3")
494 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
495 command_line.add(f"'{dirname}/trex_astf_stop.py'")
496 command_line.change_prefix(u"--")
497 for index, value in enumerate(self._xstats):
498 if value is not None:
499 value = value.replace(u"'", u"\"")
500 command_line.add_equals(f"xstat{index}", f"'{value}'")
501 stdout, _ = exec_cmd_no_error(
503 message=u"T-Rex ASTF runtime error!"
505 self._parse_traffic_results(stdout)
507 def trex_stl_stop_remote_exec(self, node):
508 """Execute T-Rex STL script on remote node over ssh to stop running
511 Internal state is updated with measurement results.
513 :param node: T-Rex generator node.
515 :raises RuntimeError: If stop traffic script fails.
517 command_line = OptionString().add(u"python3")
518 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
519 command_line.add(f"'{dirname}/trex_stl_stop.py'")
520 command_line.change_prefix(u"--")
521 for index, value in enumerate(self._xstats):
522 if value is not None:
523 value = value.replace(u"'", u"\"")
524 command_line.add_equals(f"xstat{index}", f"'{value}'")
525 stdout, _ = exec_cmd_no_error(
527 message=u"T-Rex STL runtime error!"
529 self._parse_traffic_results(stdout)
531 def stop_traffic_on_tg(self):
532 """Stop all traffic on TG.
534 :returns: Structure containing the result of the measurement.
535 :rtype: ReceiveRateMeasurement
536 :raises ValueError: If TG traffic profile is not supported.
538 subtype = check_subtype(self._node)
539 if subtype != NodeSubTypeTG.TREX:
540 raise ValueError(f"Unsupported TG subtype: {subtype!r}")
541 if u"trex-astf" in self.traffic_profile:
542 self.trex_astf_stop_remote_exec(self._node)
543 elif u"trex-stl" in self.traffic_profile:
544 self.trex_stl_stop_remote_exec(self._node)
546 raise ValueError(u"Unsupported T-Rex traffic profile!")
547 self._stop_time = time.monotonic()
549 return self._get_measurement_result()
551 def _compute_duration(self, duration, multiplier):
552 """Compute duration for profile driver.
554 The final result is influenced by transaction scale and duration limit.
555 It is assumed a higher level function has already set those to self.
556 The duration argument is the target value from search point of view,
557 before the overrides are applied here.
559 Minus one (signalling async traffic start) is kept.
561 Completeness flag is also included. Duration limited or async trials
562 are not considered complete for ramp-up purposes.
564 :param duration: Time expressed in seconds for how long to send traffic.
565 :param multiplier: Traffic rate in transactions per second.
566 :type duration: float
567 :type multiplier: float
568 :returns: New duration and whether it was a complete ramp-up candidate.
573 return duration, False
574 computed_duration = duration
575 if self.transaction_scale:
576 computed_duration = self.transaction_scale / multiplier
577 # Log the computed duration,
578 # so we can compare with what telemetry suggests
579 # the real duration was.
580 logger.debug(f"Expected duration {computed_duration}")
581 if not self.duration_limit:
582 return computed_duration, True
583 limited_duration = min(computed_duration, self.duration_limit)
584 return limited_duration, (limited_duration == computed_duration)
586 def trex_astf_start_remote_exec(
587 self, duration, multiplier, async_call=False):
588 """Execute T-Rex ASTF script on remote node over ssh to start running
591 In sync mode, measurement results are stored internally.
592 In async mode, initial data including xstats are stored internally.
594 This method contains the logic to compute duration as maximum time
595 if transaction_scale is nonzero.
596 The transaction_scale argument defines (limits) how many transactions
597 will be started in total. As that amount of transaction can take
598 considerable time (sometimes due to explicit delays in the profile),
599 the real time a trial needs to finish is computed here. For now,
600 in that case the duration argument is ignored, assuming it comes
601 from ASTF-unaware search algorithm. The overall time a single
602 transaction needs is given in parameter transaction_duration,
603 it includes both explicit delays and implicit time it takes
604 to transfer data (or whatever the transaction does).
606 Currently it is observed TRex does not start the ASTF traffic
607 immediately, an ad-hoc constant is added to the computed duration
608 to compensate for that.
610 If transaction_scale is zero, duration is not recomputed.
611 It is assumed the subsequent result parsing gets the real duration
612 if the traffic stops sooner for any reason.
614 Currently, it is assumed traffic profile defines a single transaction.
615 To avoid heavy logic here, the input rate is expected to be in
616 transactions per second, as that directly translates to TRex multiplier,
617 (assuming the profile does not override the default cps value of one).
619 :param duration: Time expressed in seconds for how long to send traffic.
620 :param multiplier: Traffic rate in transactions per second.
621 :param async_call: If enabled then don't wait for all incoming traffic.
622 :type duration: float
623 :type multiplier: int
624 :type async_call: bool
625 :raises RuntimeError: In case of T-Rex driver issue.
627 self.check_mode(TrexMode.ASTF)
628 p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
629 if not isinstance(duration, (float, int)):
630 duration = float(duration)
632 # TODO: Refactor the code so duration is computed only once,
633 # and both the initial and the computed durations are logged.
634 computed_duration, _ = self._compute_duration(duration, multiplier)
636 command_line = OptionString().add(u"python3")
637 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
638 command_line.add(f"'{dirname}/trex_astf_profile.py'")
639 command_line.change_prefix(u"--")
640 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
641 command_line.add_with_value(
642 u"profile", f"'{dirname}/{self.traffic_profile}.py'"
644 command_line.add_with_value(u"duration", f"{computed_duration!r}")
645 command_line.add_with_value(u"frame_size", self.frame_size)
646 command_line.add_with_value(
647 u"n_data_frames", Constants.ASTF_N_DATA_FRAMES
649 command_line.add_with_value(u"multiplier", multiplier)
650 command_line.add_with_value(u"port_0", p_0)
651 command_line.add_with_value(u"port_1", p_1)
652 command_line.add_with_value(
653 u"traffic_directions", self.traffic_directions
655 command_line.add_if(u"async_start", async_call)
656 command_line.add_if(u"latency", self.use_latency)
657 command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
658 command_line.add_with_value(
659 u"delay", Constants.PERF_TRIAL_ASTF_DELAY
662 self._start_time = time.monotonic()
663 self._rate = multiplier
664 stdout, _ = exec_cmd_no_error(
665 self._node, command_line, timeout=computed_duration + 10.0,
666 message=u"T-Rex ASTF runtime error!"
671 self._target_duration = None
672 self._duration = None
673 self._received = None
677 xstats = [None, None]
678 self._l7_data = dict()
679 self._l7_data[u"client"] = dict()
680 self._l7_data[u"client"][u"active_flows"] = None
681 self._l7_data[u"client"][u"established_flows"] = None
682 self._l7_data[u"client"][u"traffic_duration"] = None
683 self._l7_data[u"server"] = dict()
684 self._l7_data[u"server"][u"active_flows"] = None
685 self._l7_data[u"server"][u"established_flows"] = None
686 self._l7_data[u"server"][u"traffic_duration"] = None
687 if u"udp" in self.traffic_profile:
688 self._l7_data[u"client"][u"udp"] = dict()
689 self._l7_data[u"client"][u"udp"][u"connects"] = None
690 self._l7_data[u"client"][u"udp"][u"closed_flows"] = None
691 self._l7_data[u"client"][u"udp"][u"err_cwf"] = None
692 self._l7_data[u"server"][u"udp"] = dict()
693 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = None
694 self._l7_data[u"server"][u"udp"][u"closed_flows"] = None
695 elif u"tcp" in self.traffic_profile:
696 self._l7_data[u"client"][u"tcp"] = dict()
697 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = None
698 self._l7_data[u"client"][u"tcp"][u"connects"] = None
699 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = None
700 self._l7_data[u"client"][u"tcp"][u"connattempt"] = None
701 self._l7_data[u"server"][u"tcp"] = dict()
702 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = None
703 self._l7_data[u"server"][u"tcp"][u"connects"] = None
704 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = None
706 logger.warn(u"Unsupported T-Rex ASTF traffic profile!")
708 for line in stdout.splitlines():
709 if f"Xstats snapshot {index}: " in line:
710 xstats[index] = line[19:]
714 self._xstats = tuple(xstats)
716 self._target_duration = duration
717 self._duration = computed_duration
718 self._parse_traffic_results(stdout)
720 def trex_stl_start_remote_exec(self, duration, rate, async_call=False):
721 """Execute T-Rex STL script on remote node over ssh to start running
724 In sync mode, measurement results are stored internally.
725 In async mode, initial data including xstats are stored internally.
727 Mode-unaware code (e.g. in search algorithms) works with transactions.
728 To keep the logic simple, multiplier is set to that value.
729 As bidirectional traffic profiles send packets in both directions,
730 they are treated as transactions with two packets (one per direction).
732 :param duration: Time expressed in seconds for how long to send traffic.
733 :param rate: Traffic rate in transactions per second.
734 :param async_call: If enabled then don't wait for all incoming traffic.
735 :type duration: float
737 :type async_call: bool
738 :raises RuntimeError: In case of T-Rex driver issue.
740 self.check_mode(TrexMode.STL)
741 p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
742 if not isinstance(duration, (float, int)):
743 duration = float(duration)
745 # TODO: Refactor the code so duration is computed only once,
746 # and both the initial and the computed durations are logged.
747 duration, _ = self._compute_duration(duration=duration, multiplier=rate)
749 command_line = OptionString().add(u"python3")
750 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
751 command_line.add(f"'{dirname}/trex_stl_profile.py'")
752 command_line.change_prefix(u"--")
753 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
754 command_line.add_with_value(
755 u"profile", f"'{dirname}/{self.traffic_profile}.py'"
757 command_line.add_with_value(u"duration", f"{duration!r}")
758 command_line.add_with_value(u"frame_size", self.frame_size)
759 command_line.add_with_value(u"rate", f"{rate!r}")
760 command_line.add_with_value(u"port_0", p_0)
761 command_line.add_with_value(u"port_1", p_1)
762 command_line.add_with_value(
763 u"traffic_directions", self.traffic_directions
765 command_line.add_if(u"async_start", async_call)
766 command_line.add_if(u"latency", self.use_latency)
767 command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
768 command_line.add_with_value(u"delay", Constants.PERF_TRIAL_STL_DELAY)
770 # TODO: This is ugly. Handle parsing better.
771 self._start_time = time.monotonic()
772 self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
773 stdout, _ = exec_cmd_no_error(
774 self._node, command_line, timeout=int(duration) + 60,
775 message=u"T-Rex STL runtime error"
780 self._target_duration = None
781 self._duration = None
782 self._received = None
787 xstats = [None, None]
789 for line in stdout.splitlines():
790 if f"Xstats snapshot {index}: " in line:
791 xstats[index] = line[19:]
795 self._xstats = tuple(xstats)
797 self._target_duration = duration
798 self._duration = duration
799 self._parse_traffic_results(stdout)
801 def send_traffic_on_tg(
809 traffic_directions=2,
810 transaction_duration=0.0,
812 transaction_type=u"packet",
816 ramp_up_duration=None,
820 """Send traffic from all configured interfaces on TG.
822 In async mode, xstats is stored internally,
823 to enable getting correct result when stopping the traffic.
824 In both modes, stdout is returned,
825 but _parse_traffic_results only works in sync output.
827 Note that traffic generator uses DPDK driver which might
828 reorder port numbers based on wiring and PCI numbering.
829 This method handles that, so argument values are invariant,
830 but you can see swapped valued in debug logs.
832 When transaction_scale is specified, the duration value is ignored
833 and the needed time is computed. For cases where this results in
834 to too long measurement (e.g. teardown trial with small rate),
835 duration_limit is applied (of non-zero), so the trial is stopped sooner.
837 Bidirectional STL profiles are treated as transactions with two packets.
839 The return value is None for async.
841 :param duration: Duration of test traffic generation in seconds.
842 :param rate: Traffic rate in transactions per second.
843 :param frame_size: Frame size (L2) in Bytes.
844 :param traffic_profile: Module name as a traffic profile identifier.
845 See GPL/traffic_profiles/trex for implemented modules.
846 :param async_call: Async mode.
847 :param ppta: Packets per transaction, aggregated over directions.
848 Needed for udp_pps which does not have a good transaction counter,
849 so we need to compute expected number of packets.
851 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
853 :param transaction_duration: Total expected time to close transaction.
854 :param transaction_scale: Number of transactions to perform.
855 0 (default) means unlimited.
856 :param transaction_type: An identifier specifying which counters
857 and formulas to use when computing attempted and failed
858 transactions. Default: "packet".
859 :param duration_limit: Zero or maximum limit for computed (or given)
861 :param use_latency: Whether to measure latency during the trial.
863 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
864 :param ramp_up_duration: Duration of ramp-up trials [s].
865 :param state_timeout: Time of life of DUT state [s].
866 :param ramp_up_only: If true, do not perform main trial measurement.
867 :type duration: float
869 :type frame_size: str
870 :type traffic_profile: str
871 :type async_call: bool
873 :type traffic_directions: int
874 :type transaction_duration: float
875 :type transaction_scale: int
876 :type transaction_type: str
877 :type duration_limit: float
878 :type use_latency: bool
879 :type ramp_up_rate: float
880 :type ramp_up_duration: float
881 :type state_timeout: float
882 :type ramp_up_only: bool
883 :returns: TG results.
884 :rtype: ReceiveRateMeasurement or None
885 :raises ValueError: If TG traffic profile is not supported.
887 self.set_rate_provider_defaults(
888 frame_size=frame_size,
889 traffic_profile=traffic_profile,
891 traffic_directions=traffic_directions,
892 transaction_duration=transaction_duration,
893 transaction_scale=transaction_scale,
894 transaction_type=transaction_type,
895 duration_limit=duration_limit,
896 use_latency=use_latency,
897 ramp_up_rate=ramp_up_rate,
898 ramp_up_duration=ramp_up_duration,
899 state_timeout=state_timeout,
901 return self._send_traffic_on_tg_with_ramp_up(
904 async_call=async_call,
905 ramp_up_only=ramp_up_only,
908 def _send_traffic_on_tg_internal(
909 self, duration, rate, async_call=False):
910 """Send traffic from all configured interfaces on TG.
912 This is an internal function, it assumes set_rate_provider_defaults
913 has been called to remember most values.
914 The reason why need to remember various values is that
915 the traffic can be asynchronous, and parsing needs those values.
916 The reason why this is is a separate function from the one
917 which calls set_rate_provider_defaults is that some search algorithms
918 need to specify their own values, and we do not want the measure call
919 to overwrite them with defaults.
921 This function is used both for automated ramp-up trials
922 and for explicitly called trials.
924 :param duration: Duration of test traffic generation in seconds.
925 :param rate: Traffic rate in transactions per second.
926 :param async_call: Async mode.
927 :type duration: float
929 :type async_call: bool
930 :returns: TG results.
931 :rtype: ReceiveRateMeasurement or None
932 :raises ValueError: If TG traffic profile is not supported.
934 subtype = check_subtype(self._node)
935 if subtype == NodeSubTypeTG.TREX:
936 if u"trex-astf" in self.traffic_profile:
937 self.trex_astf_start_remote_exec(
938 duration, float(rate), async_call
940 elif u"trex-stl" in self.traffic_profile:
941 unit_rate_str = str(rate) + u"pps"
942 # TODO: Suport transaction_scale et al?
943 self.trex_stl_start_remote_exec(
944 duration, unit_rate_str, async_call
947 raise ValueError(u"Unsupported T-Rex traffic profile!")
949 return None if async_call else self._get_measurement_result()
951 def _send_traffic_on_tg_with_ramp_up(
952 self, duration, rate, async_call=False, ramp_up_only=False):
953 """Send traffic from all interfaces on TG, maybe after ramp-up.
955 This is an internal function, it assumes set_rate_provider_defaults
956 has been called to remember most values.
957 The reason why need to remember various values is that
958 the traffic can be asynchronous, and parsing needs those values.
959 The reason why this is a separate function from the one
960 which calls set_rate_provider_defaults is that some search algorithms
961 need to specify their own values, and we do not want the measure call
962 to overwrite them with defaults.
964 If ramp-up tracking is detected, a computation is performed,
965 and if state timeout is near, trial at ramp-up rate and duration
966 is inserted before the main trial measurement.
968 The ramp_up_only parameter forces a ramp-up without immediate
969 trial measurement, which is useful in case self remembers
970 a previous ramp-up trial that belongs to a different test (phase).
972 Return None if trial is async or ramp-up only.
974 :param duration: Duration of test traffic generation in seconds.
975 :param rate: Traffic rate in transactions per second.
976 :param async_call: Async mode.
977 :param ramp_up_only: If true, do not perform main trial measurement.
978 :type duration: float
980 :type async_call: bool
981 :type ramp_up_only: bool
982 :returns: TG results.
983 :rtype: ReceiveRateMeasurement or None
984 :raises ValueError: If TG traffic profile is not supported.
987 if self.ramp_up_rate:
988 # Figure out whether we need to insert a ramp-up trial.
989 # TODO: Give up on async_call=True?
990 if ramp_up_only or self.ramp_up_start is None:
991 # We never ramped up yet (at least not in this test case).
992 ramp_up_needed = True
994 # We ramped up before, but maybe it was too long ago.
995 # Adding a constant overhead to be safe.
996 time_now = time.monotonic() + 1.0
997 computed_duration, complete = self._compute_duration(
1001 # There are two conditions for inserting ramp-up.
1002 # If early sessions are expiring already,
1003 # or if late sessions are to expire before measurement is over.
1004 ramp_up_start_delay = time_now - self.ramp_up_start
1005 ramp_up_stop_delay = time_now - self.ramp_up_stop
1006 ramp_up_stop_delay += computed_duration
1007 bigger_delay = max(ramp_up_start_delay, ramp_up_stop_delay)
1008 # Final boolean decision.
1009 ramp_up_needed = (bigger_delay >= self.state_timeout)
1012 u"State may time out during next real trial, "
1013 u"inserting a ramp-up trial."
1015 self.ramp_up_start = time.monotonic()
1016 self._send_traffic_on_tg_internal(
1017 duration=self.ramp_up_duration,
1018 rate=self.ramp_up_rate,
1019 async_call=async_call,
1021 self.ramp_up_stop = time.monotonic()
1022 logger.debug(u"Ramp-up done.")
1025 u"State will probably not time out during next real trial, "
1026 u"no ramp-up trial needed just yet."
1030 trial_start = time.monotonic()
1031 result = self._send_traffic_on_tg_internal(
1034 async_call=async_call,
1036 trial_end = time.monotonic()
1037 if self.ramp_up_rate:
1038 # Optimization: No loss acts as a good ramp-up, if it was complete.
1039 if complete and result is not None and result.loss_count == 0:
1040 logger.debug(u"Good trial acts as a ramp-up")
1041 self.ramp_up_start = trial_start
1042 self.ramp_up_stop = trial_end
1044 logger.debug(u"Loss or incomplete, does not act as a ramp-up.")
1047 def no_traffic_loss_occurred(self):
1048 """Fail if loss occurred in traffic run.
1051 :raises Exception: If loss occured.
1053 if self._loss is None:
1054 raise RuntimeError(u"The traffic generation has not been issued")
1055 if self._loss != u"0":
1056 raise RuntimeError(f"Traffic loss occurred: {self._loss}")
1058 def fail_if_no_traffic_forwarded(self):
1059 """Fail if no traffic forwarded.
1061 TODO: Check number of passed transactions instead.
1064 :raises Exception: If no traffic forwarded.
1066 if self._received is None:
1067 raise RuntimeError(u"The traffic generation has not been issued")
1068 if self._received == 0:
1069 raise RuntimeError(u"No traffic forwarded")
1071 def partial_traffic_loss_accepted(
1072 self, loss_acceptance, loss_acceptance_type):
1073 """Fail if loss is higher then accepted in traffic run.
1075 :param loss_acceptance: Permitted drop ratio or frames count.
1076 :param loss_acceptance_type: Type of permitted loss.
1077 :type loss_acceptance: float
1078 :type loss_acceptance_type: LossAcceptanceType
1080 :raises Exception: If loss is above acceptance criteria.
1082 if self._loss is None:
1083 raise Exception(u"The traffic generation has not been issued")
1085 if loss_acceptance_type == u"percentage":
1086 loss = (float(self._loss) / float(self._sent)) * 100
1087 elif loss_acceptance_type == u"frames":
1088 loss = float(self._loss)
1090 raise Exception(u"Loss acceptance type not supported")
1092 if loss > float(loss_acceptance):
1094 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
1097 def _parse_traffic_results(self, stdout):
1098 """Parse stdout of scripts into fields of self.
1100 Block of code to reuse, by sync start, or stop after async.
1102 :param stdout: Text containing the standard output.
1105 subtype = check_subtype(self._node)
1106 if subtype == NodeSubTypeTG.TREX:
1107 # Last line from console output
1108 line = stdout.splitlines()[-1]
1109 results = line.split(u";")
1110 if results[-1] in (u" ", u""):
1112 self._result = dict()
1113 for result in results:
1114 key, value = result.split(u"=", maxsplit=1)
1115 self._result[key.strip()] = value
1116 logger.info(f"TrafficGen results:\n{self._result}")
1117 self._received = int(self._result.get(u"total_received"), 0)
1118 self._sent = int(self._result.get(u"total_sent", 0))
1119 self._loss = int(self._result.get(u"frame_loss", 0))
1120 self._approximated_duration = \
1121 self._result.get(u"approximated_duration", 0.0)
1122 if u"manual" not in str(self._approximated_duration):
1123 self._approximated_duration = float(self._approximated_duration)
1124 self._latency = list()
1125 self._latency.append(self._result.get(u"latency_stream_0(usec)"))
1126 self._latency.append(self._result.get(u"latency_stream_1(usec)"))
1127 if self._mode == TrexMode.ASTF:
1128 self._l7_data = dict()
1129 self._l7_data[u"client"] = dict()
1130 self._l7_data[u"client"][u"sent"] = \
1131 int(self._result.get(u"client_sent", 0))
1132 self._l7_data[u"client"][u"received"] = \
1133 int(self._result.get(u"client_received", 0))
1134 self._l7_data[u"client"][u"active_flows"] = \
1135 int(self._result.get(u"client_active_flows", 0))
1136 self._l7_data[u"client"][u"established_flows"] = \
1137 int(self._result.get(u"client_established_flows", 0))
1138 self._l7_data[u"client"][u"traffic_duration"] = \
1139 float(self._result.get(u"client_traffic_duration", 0.0))
1140 self._l7_data[u"client"][u"err_rx_throttled"] = \
1141 int(self._result.get(u"client_err_rx_throttled", 0))
1142 self._l7_data[u"client"][u"err_c_nf_throttled"] = \
1143 int(self._result.get(u"client_err_nf_throttled", 0))
1144 self._l7_data[u"client"][u"err_flow_overflow"] = \
1145 int(self._result.get(u"client_err_flow_overflow", 0))
1146 self._l7_data[u"server"] = dict()
1147 self._l7_data[u"server"][u"active_flows"] = \
1148 int(self._result.get(u"server_active_flows", 0))
1149 self._l7_data[u"server"][u"established_flows"] = \
1150 int(self._result.get(u"server_established_flows", 0))
1151 self._l7_data[u"server"][u"traffic_duration"] = \
1152 float(self._result.get(u"server_traffic_duration", 0.0))
1153 self._l7_data[u"server"][u"err_rx_throttled"] = \
1154 int(self._result.get(u"client_err_rx_throttled", 0))
1155 if u"udp" in self.traffic_profile:
1156 self._l7_data[u"client"][u"udp"] = dict()
1157 self._l7_data[u"client"][u"udp"][u"connects"] = \
1158 int(self._result.get(u"client_udp_connects", 0))
1159 self._l7_data[u"client"][u"udp"][u"closed_flows"] = \
1160 int(self._result.get(u"client_udp_closed", 0))
1161 self._l7_data[u"client"][u"udp"][u"tx_bytes"] = \
1162 int(self._result.get(u"client_udp_tx_bytes", 0))
1163 self._l7_data[u"client"][u"udp"][u"rx_bytes"] = \
1164 int(self._result.get(u"client_udp_rx_bytes", 0))
1165 self._l7_data[u"client"][u"udp"][u"tx_packets"] = \
1166 int(self._result.get(u"client_udp_tx_packets", 0))
1167 self._l7_data[u"client"][u"udp"][u"rx_packets"] = \
1168 int(self._result.get(u"client_udp_rx_packets", 0))
1169 self._l7_data[u"client"][u"udp"][u"keep_drops"] = \
1170 int(self._result.get(u"client_udp_keep_drops", 0))
1171 self._l7_data[u"client"][u"udp"][u"err_cwf"] = \
1172 int(self._result.get(u"client_err_cwf", 0))
1173 self._l7_data[u"server"][u"udp"] = dict()
1174 self._l7_data[u"server"][u"udp"][u"accepted_flows"] = \
1175 int(self._result.get(u"server_udp_accepts", 0))
1176 self._l7_data[u"server"][u"udp"][u"closed_flows"] = \
1177 int(self._result.get(u"server_udp_closed", 0))
1178 self._l7_data[u"server"][u"udp"][u"tx_bytes"] = \
1179 int(self._result.get(u"server_udp_tx_bytes", 0))
1180 self._l7_data[u"server"][u"udp"][u"rx_bytes"] = \
1181 int(self._result.get(u"server_udp_rx_bytes", 0))
1182 self._l7_data[u"server"][u"udp"][u"tx_packets"] = \
1183 int(self._result.get(u"server_udp_tx_packets", 0))
1184 self._l7_data[u"server"][u"udp"][u"rx_packets"] = \
1185 int(self._result.get(u"server_udp_rx_packets", 0))
1186 elif u"tcp" in self.traffic_profile:
1187 self._l7_data[u"client"][u"tcp"] = dict()
1188 self._l7_data[u"client"][u"tcp"][u"initiated_flows"] = \
1189 int(self._result.get(u"client_tcp_connect_inits", 0))
1190 self._l7_data[u"client"][u"tcp"][u"connects"] = \
1191 int(self._result.get(u"client_tcp_connects", 0))
1192 self._l7_data[u"client"][u"tcp"][u"closed_flows"] = \
1193 int(self._result.get(u"client_tcp_closed", 0))
1194 self._l7_data[u"client"][u"tcp"][u"connattempt"] = \
1195 int(self._result.get(u"client_tcp_connattempt", 0))
1196 self._l7_data[u"client"][u"tcp"][u"tx_bytes"] = \
1197 int(self._result.get(u"client_tcp_tx_bytes", 0))
1198 self._l7_data[u"client"][u"tcp"][u"rx_bytes"] = \
1199 int(self._result.get(u"client_tcp_rx_bytes", 0))
1200 self._l7_data[u"server"][u"tcp"] = dict()
1201 self._l7_data[u"server"][u"tcp"][u"accepted_flows"] = \
1202 int(self._result.get(u"server_tcp_accepts", 0))
1203 self._l7_data[u"server"][u"tcp"][u"connects"] = \
1204 int(self._result.get(u"server_tcp_connects", 0))
1205 self._l7_data[u"server"][u"tcp"][u"closed_flows"] = \
1206 int(self._result.get(u"server_tcp_closed", 0))
1207 self._l7_data[u"server"][u"tcp"][u"tx_bytes"] = \
1208 int(self._result.get(u"server_tcp_tx_bytes", 0))
1209 self._l7_data[u"server"][u"tcp"][u"rx_bytes"] = \
1210 int(self._result.get(u"server_tcp_rx_bytes", 0))
1212 def _get_measurement_result(self):
1213 """Return the result of last measurement as ReceiveRateMeasurement.
1215 Separate function, as measurements can end either by time
1216 or by explicit call, this is the common block at the end.
1218 The target_tr field of ReceiveRateMeasurement is in
1219 transactions per second. Transmit count and loss count units
1220 depend on the transaction type. Usually they are in transactions
1221 per second, or aggregated packets per second.
1223 TODO: Fail on running or already reported measurement.
1225 :returns: Structure containing the result of the measurement.
1226 :rtype: ReceiveRateMeasurement
1229 # Client duration seems to include a setup period
1230 # where TRex does not send any packets yet.
1231 # Server duration does not include it.
1232 server_data = self._l7_data[u"server"]
1233 approximated_duration = float(server_data[u"traffic_duration"])
1234 except (KeyError, AttributeError, ValueError, TypeError):
1235 approximated_duration = None
1237 if not approximated_duration:
1238 approximated_duration = float(self._approximated_duration)
1239 except ValueError: # "manual"
1240 approximated_duration = None
1241 if not approximated_duration:
1242 if self._duration and self._duration > 0:
1243 # Known recomputed or target duration.
1244 approximated_duration = self._duration
1246 # It was an explicit stop.
1247 if not self._stop_time:
1248 raise RuntimeError(u"Unable to determine duration.")
1249 approximated_duration = self._stop_time - self._start_time
1250 target_duration = self._target_duration
1251 if not target_duration:
1252 target_duration = approximated_duration
1253 transmit_rate = self._rate
1255 if self.transaction_type == u"packet":
1256 partial_attempt_count = self._sent
1257 packet_rate = transmit_rate * self.ppta
1258 # We have a float. TRex way of rounding it is not obvious.
1259 # The biggest source of mismatch is Inter Stream Gap.
1260 # So the code tolerates 10 usec of missing packets.
1261 expected_attempt_count = (target_duration - 1e-5) * packet_rate
1262 expected_attempt_count = math.ceil(expected_attempt_count)
1263 # TRex can send more.
1264 expected_attempt_count = max(expected_attempt_count, self._sent)
1265 unsent = expected_attempt_count - self._sent
1266 pass_count = self._received
1267 fail_count = expected_attempt_count - pass_count
1268 elif self.transaction_type == u"udp_cps":
1269 if not self.transaction_scale:
1270 raise RuntimeError(u"Add support for no-limit udp_cps.")
1271 partial_attempt_count = self._l7_data[u"client"][u"sent"]
1272 # We do not care whether TG is slow, it should have attempted all.
1273 expected_attempt_count = self.transaction_scale
1274 unsent = expected_attempt_count - partial_attempt_count
1275 pass_count = self._l7_data[u"client"][u"received"]
1276 fail_count = expected_attempt_count - pass_count
1277 elif self.transaction_type == u"tcp_cps":
1278 if not self.transaction_scale:
1279 raise RuntimeError(u"Add support for no-limit tcp_cps.")
1280 ctca = self._l7_data[u"client"][u"tcp"][u"connattempt"]
1281 partial_attempt_count = ctca
1282 # We do not care whether TG is slow, it should have attempted all.
1283 expected_attempt_count = self.transaction_scale
1284 unsent = expected_attempt_count - partial_attempt_count
1285 # From TCP point of view, server/connects counts full connections,
1286 # but we are testing NAT session so client/connects counts that
1287 # (half connections from TCP point of view).
1288 pass_count = self._l7_data[u"client"][u"tcp"][u"connects"]
1289 fail_count = expected_attempt_count - pass_count
1290 elif self.transaction_type == u"udp_pps":
1291 if not self.transaction_scale:
1292 raise RuntimeError(u"Add support for no-limit udp_pps.")
1293 partial_attempt_count = self._sent
1294 expected_attempt_count = self.transaction_scale * self.ppta
1295 unsent = expected_attempt_count - self._sent
1296 fail_count = self._loss + unsent
1297 elif self.transaction_type == u"tcp_pps":
1298 if not self.transaction_scale:
1299 raise RuntimeError(u"Add support for no-limit tcp_pps.")
1300 partial_attempt_count = self._sent
1301 expected_attempt_count = self.transaction_scale * self.ppta
1302 # One loss-like scenario happens when TRex receives all packets
1303 # on L2 level, but is not fast enough to process them all
1304 # at L7 level, which leads to retransmissions.
1305 # Those manifest as opackets larger than expected.
1306 # A simple workaround is to add absolute difference.
1307 # Probability of retransmissions exactly cancelling
1308 # packets unsent due to duration stretching is quite low.
1309 unsent = abs(expected_attempt_count - self._sent)
1310 fail_count = self._loss + unsent
1312 raise RuntimeError(f"Unknown parsing {self.transaction_type!r}")
1313 if unsent and isinstance(self._approximated_duration, float):
1314 # Do not report unsent for "manual".
1315 logger.debug(f"Unsent packets/transactions: {unsent}")
1316 if fail_count < 0 and not self.negative_loss:
1318 measurement = ReceiveRateMeasurement(
1319 duration=target_duration,
1320 target_tr=transmit_rate,
1321 transmit_count=expected_attempt_count,
1322 loss_count=fail_count,
1323 approximated_duration=approximated_duration,
1324 partial_transmit_count=partial_attempt_count,
1326 measurement.latency = self.get_latency_int()
1329 def measure(self, duration, transmit_rate):
1330 """Run trial measurement, parse and return results.
1332 The input rate is for transactions. Stateles bidirectional traffic
1333 is understood as sequence of (asynchronous) transactions,
1336 The result units depend on test type, generally
1337 the count either transactions or packets (aggregated over directions).
1339 Optionally, this method sleeps if measurement finished before
1340 the time specified as duration.
1342 :param duration: Trial duration [s].
1343 :param transmit_rate: Target rate in transactions per second.
1344 :type duration: float
1345 :type transmit_rate: float
1346 :returns: Structure containing the result of the measurement.
1347 :rtype: ReceiveRateMeasurement
1348 :raises RuntimeError: If TG is not set or if node is not TG
1349 or if subtype is not specified.
1350 :raises NotImplementedError: If TG is not supported.
1352 duration = float(duration)
1353 time_start = time.monotonic()
1354 time_stop = time_start + duration
1357 result = self._send_traffic_on_tg_with_ramp_up(
1362 logger.debug(f"trial measurement result: {result!r}")
1363 # In PLRsearch, computation needs the specified time to complete.
1364 if self.sleep_till_duration:
1365 sleeptime = time_stop - time.monotonic()
1367 # TODO: Sometimes we have time to do additional trials here,
1368 # adapt PLRsearch to accept all the results.
1369 time.sleep(sleeptime)
1372 def set_rate_provider_defaults(
1378 traffic_directions=2,
1379 transaction_duration=0.0,
1380 transaction_scale=0,
1381 transaction_type=u"packet",
1384 sleep_till_duration=False,
1387 ramp_up_duration=None,
1388 state_timeout=240.0,
1390 """Store values accessed by measure().
1392 :param frame_size: Frame size identifier or value [B].
1393 :param traffic_profile: Module name as a traffic profile identifier.
1394 See GPL/traffic_profiles/trex for implemented modules.
1395 :param ppta: Packets per transaction, aggregated over directions.
1396 Needed for udp_pps which does not have a good transaction counter,
1397 so we need to compute expected number of packets.
1399 :param resetter: Callable to reset DUT state for repeated trials.
1400 :param traffic_directions: Traffic from packet counting point of view
1401 is bi- (2) or uni- (1) directional.
1403 :param transaction_duration: Total expected time to close transaction.
1404 :param transaction_scale: Number of transactions to perform.
1405 0 (default) means unlimited.
1406 :param transaction_type: An identifier specifying which counters
1407 and formulas to use when computing attempted and failed
1408 transactions. Default: "packet".
1409 TODO: Does this also specify parsing for the measured duration?
1410 :param duration_limit: Zero or maximum limit for computed (or given)
1412 :param negative_loss: If false, negative loss is reported as zero loss.
1413 :param sleep_till_duration: If true and measurement returned faster,
1414 sleep until it matches duration. Needed for PLRsearch.
1415 :param use_latency: Whether to measure latency during the trial.
1417 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1418 :param ramp_up_duration: Duration of ramp-up trials [s].
1419 :param state_timeout: Time of life of DUT state [s].
1420 :type frame_size: str or int
1421 :type traffic_profile: str
1423 :type resetter: Optional[Callable[[], None]]
1424 :type traffic_directions: int
1425 :type transaction_duration: float
1426 :type transaction_scale: int
1427 :type transaction_type: str
1428 :type duration_limit: float
1429 :type negative_loss: bool
1430 :type sleep_till_duration: bool
1431 :type use_latency: bool
1432 :type ramp_up_rate: float
1433 :type ramp_up_duration: float
1434 :type state_timeout: float
1436 self.frame_size = frame_size
1437 self.traffic_profile = str(traffic_profile)
1438 self.resetter = resetter
1440 self.traffic_directions = int(traffic_directions)
1441 self.transaction_duration = float(transaction_duration)
1442 self.transaction_scale = int(transaction_scale)
1443 self.transaction_type = str(transaction_type)
1444 self.duration_limit = float(duration_limit)
1445 self.negative_loss = bool(negative_loss)
1446 self.sleep_till_duration = bool(sleep_till_duration)
1447 self.use_latency = bool(use_latency)
1448 self.ramp_up_rate = float(ramp_up_rate)
1449 self.ramp_up_duration = float(ramp_up_duration)
1450 self.state_timeout = float(state_timeout)
1453 class OptimizedSearch:
1454 """Class to be imported as Robot Library, containing search keywords.
1456 Aside of setting up measurer and forwarding arguments,
1457 the main business is to translate min/max rate from unidir to aggregated.
1461 def perform_optimized_ndrpdr_search(
1464 minimum_transmit_rate,
1465 maximum_transmit_rate,
1466 packet_loss_ratio=0.005,
1467 final_relative_width=0.005,
1468 final_trial_duration=30.0,
1469 initial_trial_duration=1.0,
1470 number_of_intermediate_phases=2,
1474 traffic_directions=2,
1475 transaction_duration=0.0,
1476 transaction_scale=0,
1477 transaction_type=u"packet",
1480 ramp_up_duration=None,
1481 state_timeout=240.0,
1482 expansion_coefficient=4.0,
1484 """Setup initialized TG, perform optimized search, return intervals.
1486 If transaction_scale is nonzero, all init and non-init trial durations
1487 are set to 1.0 (as they do not affect the real trial duration)
1488 and zero intermediate phases are used.
1489 This way no re-measurement happens.
1490 Warmup has to be handled via resetter or ramp-up mechanisms.
1492 :param frame_size: Frame size identifier or value [B].
1493 :param traffic_profile: Module name as a traffic profile identifier.
1494 See GPL/traffic_profiles/trex for implemented modules.
1495 :param minimum_transmit_rate: Minimal load in transactions per second.
1496 :param maximum_transmit_rate: Maximal load in transactions per second.
1497 :param packet_loss_ratio: Ratio of packets lost, for PDR [1].
1498 :param final_relative_width: Final lower bound transmit rate
1499 cannot be more distant that this multiple of upper bound [1].
1500 :param final_trial_duration: Trial duration for the final phase [s].
1501 :param initial_trial_duration: Trial duration for the initial phase
1502 and also for the first intermediate phase [s].
1503 :param number_of_intermediate_phases: Number of intermediate phases
1504 to perform before the final phase [1].
1505 :param timeout: The search will fail itself when not finished
1506 before this overall time [s].
1507 :param ppta: Packets per transaction, aggregated over directions.
1508 Needed for udp_pps which does not have a good transaction counter,
1509 so we need to compute expected number of packets.
1511 :param resetter: Callable to reset DUT state for repeated trials.
1512 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1514 :param transaction_duration: Total expected time to close transaction.
1515 :param transaction_scale: Number of transactions to perform.
1516 0 (default) means unlimited.
1517 :param transaction_type: An identifier specifying which counters
1518 and formulas to use when computing attempted and failed
1519 transactions. Default: "packet".
1520 :param use_latency: Whether to measure latency during the trial.
1522 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1523 :param ramp_up_duration: Duration of ramp-up trials [s].
1524 :param state_timeout: Time of life of DUT state [s].
1525 :param expansion_coefficient: In external search multiply width by this.
1526 :type frame_size: str or int
1527 :type traffic_profile: str
1528 :type minimum_transmit_rate: float
1529 :type maximum_transmit_rate: float
1530 :type packet_loss_ratio: float
1531 :type final_relative_width: float
1532 :type final_trial_duration: float
1533 :type initial_trial_duration: float
1534 :type number_of_intermediate_phases: int
1535 :type timeout: float
1537 :type resetter: Optional[Callable[[], None]]
1538 :type traffic_directions: int
1539 :type transaction_duration: float
1540 :type transaction_scale: int
1541 :type transaction_type: str
1542 :type use_latency: bool
1543 :type ramp_up_rate: float
1544 :type ramp_up_duration: float
1545 :type state_timeout: float
1546 :type expansion_coefficient: float
1547 :returns: Structure containing narrowed down NDR and PDR intervals
1548 and their measurements.
1549 :rtype: List[Receiverateinterval]
1550 :raises RuntimeError: If total duration is larger than timeout.
1552 # we need instance of TrafficGenerator instantiated by Robot Framework
1553 # to be able to use trex_stl-*()
1554 tg_instance = BuiltIn().get_library_instance(
1555 u"resources.libraries.python.TrafficGenerator"
1557 # Overrides for fixed transaction amount.
1558 # TODO: Move to robot code? We have two call sites, so this saves space,
1559 # even though this is surprising for log readers.
1560 if transaction_scale:
1561 initial_trial_duration = 1.0
1562 final_trial_duration = 1.0
1563 number_of_intermediate_phases = 0
1564 timeout += transaction_scale * 3e-4
1565 tg_instance.set_rate_provider_defaults(
1566 frame_size=frame_size,
1567 traffic_profile=traffic_profile,
1568 sleep_till_duration=False,
1571 traffic_directions=traffic_directions,
1572 transaction_duration=transaction_duration,
1573 transaction_scale=transaction_scale,
1574 transaction_type=transaction_type,
1575 use_latency=use_latency,
1576 ramp_up_rate=ramp_up_rate,
1577 ramp_up_duration=ramp_up_duration,
1578 state_timeout=state_timeout,
1580 algorithm = MultipleLossRatioSearch(
1581 measurer=tg_instance,
1582 final_trial_duration=final_trial_duration,
1583 final_relative_width=final_relative_width,
1584 number_of_intermediate_phases=number_of_intermediate_phases,
1585 initial_trial_duration=initial_trial_duration,
1588 expansion_coefficient=expansion_coefficient,
1590 if packet_loss_ratio:
1591 packet_loss_ratios = [0.0, packet_loss_ratio]
1593 # Happens in reconf tests.
1594 packet_loss_ratios = [packet_loss_ratio]
1595 results = algorithm.narrow_down_intervals(
1596 min_rate=minimum_transmit_rate,
1597 max_rate=maximum_transmit_rate,
1598 packet_loss_ratios=packet_loss_ratios,
1603 def perform_soak_search(
1606 minimum_transmit_rate,
1607 maximum_transmit_rate,
1614 trace_enabled=False,
1615 traffic_directions=2,
1616 transaction_duration=0.0,
1617 transaction_scale=0,
1618 transaction_type=u"packet",
1621 ramp_up_duration=None,
1622 state_timeout=240.0,
1624 """Setup initialized TG, perform soak search, return avg and stdev.
1626 :param frame_size: Frame size identifier or value [B].
1627 :param traffic_profile: Module name as a traffic profile identifier.
1628 See GPL/traffic_profiles/trex for implemented modules.
1629 :param minimum_transmit_rate: Minimal load in transactions per second.
1630 :param maximum_transmit_rate: Maximal load in transactions per second.
1631 :param plr_target: Ratio of packets lost to achieve [1].
1632 :param tdpt: Trial duration per trial.
1633 The algorithm linearly increases trial duration with trial number,
1634 this is the increment between succesive trials, in seconds.
1635 :param initial_count: Offset to apply before the first trial.
1636 For example initial_count=50 makes first trial to be 51*tdpt long.
1637 This is needed because initial "search" phase of integrator
1638 takes significant time even without any trial results.
1639 :param timeout: The search will stop after this overall time [s].
1640 :param ppta: Packets per transaction, aggregated over directions.
1641 Needed for udp_pps which does not have a good transaction counter,
1642 so we need to compute expected number of packets.
1644 :param resetter: Callable to reset DUT state for repeated trials.
1645 :param trace_enabled: True if trace enabled else False.
1646 This is very verbose tracing on numeric computations,
1647 do not use in production.
1649 :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
1651 :param transaction_duration: Total expected time to close transaction.
1652 :param transaction_scale: Number of transactions to perform.
1653 0 (default) means unlimited.
1654 :param transaction_type: An identifier specifying which counters
1655 and formulas to use when computing attempted and failed
1656 transactions. Default: "packet".
1657 :param use_latency: Whether to measure latency during the trial.
1659 :param ramp_up_rate: Rate to use in ramp-up trials [pps].
1660 :param ramp_up_duration: Duration of ramp-up trials [s].
1661 :param state_timeout: Time of life of DUT state [s].
1662 :type frame_size: str or int
1663 :type traffic_profile: str
1664 :type minimum_transmit_rate: float
1665 :type maximum_transmit_rate: float
1666 :type plr_target: float
1667 :type initial_count: int
1668 :type timeout: float
1670 :type resetter: Optional[Callable[[], None]]
1671 :type trace_enabled: bool
1672 :type traffic_directions: int
1673 :type transaction_duration: float
1674 :type transaction_scale: int
1675 :type transaction_type: str
1676 :type use_latency: bool
1677 :type ramp_up_rate: float
1678 :type ramp_up_duration: float
1679 :type state_timeout: float
1680 :returns: Average and stdev of estimated aggregated rate giving PLR.
1681 :rtype: 2-tuple of float
1683 tg_instance = BuiltIn().get_library_instance(
1684 u"resources.libraries.python.TrafficGenerator"
1686 # Overrides for fixed transaction amount.
1687 # TODO: Move to robot code? We have a single call site
1688 # but MLRsearch has two and we want the two to be used similarly.
1689 if transaction_scale:
1690 # TODO: What is a good value for max scale?
1691 # TODO: Scale the timeout with transaction scale.
1693 tg_instance.set_rate_provider_defaults(
1694 frame_size=frame_size,
1695 traffic_profile=traffic_profile,
1696 negative_loss=False,
1697 sleep_till_duration=True,
1700 traffic_directions=traffic_directions,
1701 transaction_duration=transaction_duration,
1702 transaction_scale=transaction_scale,
1703 transaction_type=transaction_type,
1704 use_latency=use_latency,
1705 ramp_up_rate=ramp_up_rate,
1706 ramp_up_duration=ramp_up_duration,
1707 state_timeout=state_timeout,
1709 algorithm = PLRsearch(
1710 measurer=tg_instance,
1711 trial_duration_per_trial=tdpt,
1712 packet_loss_ratio_target=plr_target,
1713 trial_number_offset=initial_count,
1715 trace_enabled=trace_enabled,
1717 result = algorithm.search(
1718 min_rate=minimum_transmit_rate,
1719 max_rate=maximum_transmit_rate,