* [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework @ 2025-04-23 19:40 Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 1/5] dts: rework config module to support perf TGs Nicholas Pratte ` (6 more replies) 0 siblings, 7 replies; 35+ messages in thread From: Nicholas Pratte @ 2025-04-23 19:40 UTC (permalink / raw) To: ian.stokes, yoan.picchi, probb, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen Cc: dev, Nicholas Pratte Included in a semi-complete, RFC implementation for a would-be implementation of the TREX traffic generator, in addition to a mock implementation of a single core performance test suite, leveraging newly added performance test API functionality. Code is incomplete with a handful of typing issues, but does execute without errors on my system. Nicholas Pratte (5): dts: rework config module to support perf TGs dts: rework traffic generator inheritance structure. dts: add asychronous support to ssh sessions. dts: add trex traffic generator to dts framework dts: add performance test functions to test suite api dts/{ => configurations}/nodes.example.yaml | 0 .../test_run.example.yaml | 8 +- .../tests_config.example.yaml | 0 .../trex_configs/intel_40g.yaml | 18 ++ dts/framework/config/test_run.py | 20 +- dts/framework/context.py | 11 +- dts/framework/remote_session/ssh_session.py | 17 ++ dts/framework/settings.py | 6 +- dts/framework/test_run.py | 28 +- dts/framework/test_suite.py | 33 +- .../traffic_generator/__init__.py | 22 +- .../capturing_traffic_generator.py | 34 +++ .../performance_traffic_generator.py | 62 ++++ .../traffic_generator/traffic_generator.py | 43 +-- .../testbed_model/traffic_generator/trex.py | 287 ++++++++++++++++++ dts/tests/TestSuite_single_core_perf.py | 24 ++ 16 files changed, 550 insertions(+), 63 deletions(-) rename dts/{ => configurations}/nodes.example.yaml (100%) rename dts/{ => configurations}/test_run.example.yaml (82%) rename dts/{ => configurations}/tests_config.example.yaml (100%) create mode 100644 dts/configurations/trex_configs/intel_40g.yaml create mode 100644 dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py create mode 100644 dts/framework/testbed_model/traffic_generator/trex.py create mode 100644 dts/tests/TestSuite_single_core_perf.py -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC Patch v1 1/5] dts: rework config module to support perf TGs 2025-04-23 19:40 [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework Nicholas Pratte @ 2025-04-23 19:40 ` Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 2/5] dts: rework traffic generator inheritance structure Nicholas Pratte ` (5 subsequent siblings) 6 siblings, 0 replies; 35+ messages in thread From: Nicholas Pratte @ 2025-04-23 19:40 UTC (permalink / raw) To: ian.stokes, yoan.picchi, probb, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen Cc: dev, Nicholas Pratte Rework test run configuration file for TGs to support both application directory location and any necessary configuration files; an example TREX configuration file is provided. Configuration files have been moved to a configurations directory, requiring a slight modification to the settings module. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/{ => configurations}/nodes.example.yaml | 0 dts/{ => configurations}/test_run.example.yaml | 8 +++++++- .../tests_config.example.yaml | 0 dts/configurations/trex_configs/intel_40g.yaml | 18 ++++++++++++++++++ dts/framework/settings.py | 6 ++++-- 5 files changed, 29 insertions(+), 3 deletions(-) rename dts/{ => configurations}/nodes.example.yaml (100%) rename dts/{ => configurations}/test_run.example.yaml (82%) rename dts/{ => configurations}/tests_config.example.yaml (100%) create mode 100644 dts/configurations/trex_configs/intel_40g.yaml diff --git a/dts/nodes.example.yaml b/dts/configurations/nodes.example.yaml similarity index 100% rename from dts/nodes.example.yaml rename to dts/configurations/nodes.example.yaml diff --git a/dts/test_run.example.yaml b/dts/configurations/test_run.example.yaml similarity index 82% rename from dts/test_run.example.yaml rename to dts/configurations/test_run.example.yaml index 330a31bb18..fdfff4a879 100644 --- a/dts/test_run.example.yaml +++ b/dts/configurations/test_run.example.yaml @@ -23,8 +23,14 @@ dpdk: # in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options` # to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be # defined, but not both. -traffic_generator: +func_traffic_generator: type: SCAPY + remote_path: "" # The remote path of the traffic generator application. (Leave blank for SCAPY) + config: "" # Additional configuration files. (Leave blank if not required) +perf_traffic_generator: + type: TREX + remote_path: "/opt/trex/v3.03" # The remote path of the traffic generator application. (Leave blank for SCAPY) + config: "trex_config.yaml" # Additional configuration files. (Leave blank if not required) perf: false # disable performance testing func: true # enable functional testing skip_smoke_tests: false # optional diff --git a/dts/tests_config.example.yaml b/dts/configurations/tests_config.example.yaml similarity index 100% rename from dts/tests_config.example.yaml rename to dts/configurations/tests_config.example.yaml diff --git a/dts/configurations/trex_configs/intel_40g.yaml b/dts/configurations/trex_configs/intel_40g.yaml new file mode 100644 index 0000000000..dae003795b --- /dev/null +++ b/dts/configurations/trex_configs/intel_40g.yaml @@ -0,0 +1,18 @@ +### Config file generated by dpdk_setup_ports.py ### + +- version: 2 + interfaces: ['11:00.0', '11:00.1'] + port_bandwidth_gb: 40 + port_info: + - dest_mac: 3c:fd:fe:d5:e5:f9 # MAC OF LOOPBACK TO IT'S DUAL INTERFACE + src_mac: 3c:fd:fe:d5:e5:f8 + - dest_mac: 3c:fd:fe:d5:e5:f8 # MAC OF LOOPBACK TO IT'S DUAL INTERFACE + src_mac: 3c:fd:fe:d5:e5:f9 + + platform: + master_thread_id: 0 + latency_thread_id: 7 + dual_if: + - socket: 0 + threads: [1,2,3,4,5,6] + diff --git a/dts/framework/settings.py b/dts/framework/settings.py index 3f21615223..ccf4df25b0 100644 --- a/dts/framework/settings.py +++ b/dts/framework/settings.py @@ -130,9 +130,11 @@ class Settings: """ #: - test_run_config_path: Path = Path(__file__).parent.parent.joinpath("test_run.yaml") + test_run_config_path: Path = Path(__file__).parent.parent.joinpath( + "configurations/test_run.yaml" + ) #: - nodes_config_path: Path = Path(__file__).parent.parent.joinpath("nodes.yaml") + nodes_config_path: Path = Path(__file__).parent.parent.joinpath("configurations/nodes.yaml") #: tests_config_path: Path | None = None #: -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC Patch v1 2/5] dts: rework traffic generator inheritance structure. 2025-04-23 19:40 [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 1/5] dts: rework config module to support perf TGs Nicholas Pratte @ 2025-04-23 19:40 ` Nicholas Pratte 2025-05-15 19:24 ` Patrick Robb 2025-04-23 19:40 ` [RFC Patch v1 3/5] dts: add asychronous support to ssh sessions Nicholas Pratte ` (4 subsequent siblings) 6 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-04-23 19:40 UTC (permalink / raw) To: ian.stokes, yoan.picchi, probb, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen Cc: dev, Nicholas Pratte Rework TG class hierarchy to include performance traffic generators, in addition to capturing traffic generators. As such, methods garnered to capturing traffic have been moved to the CapturingTrafficGenerator subclass. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- .../capturing_traffic_generator.py | 34 ++++++++++ .../performance_traffic_generator.py | 62 +++++++++++++++++++ .../traffic_generator/traffic_generator.py | 43 +------------ 3 files changed, 97 insertions(+), 42 deletions(-) create mode 100644 dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py diff --git a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py index e31ba2a9b7..41b70f7f48 100644 --- a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py +++ b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py @@ -63,6 +63,40 @@ def is_capturing(self) -> bool: """This traffic generator can capture traffic.""" return True + def send_packet(self, packet: Packet, port: Port) -> None: + """Send `packet` and block until it is fully sent. + + Send `packet` on `port`, then wait until `packet` is fully sent. + + Args: + packet: The packet to send. + port: The egress port on the TG node. + """ + self.send_packets([packet], port) + + def send_packets(self, packets: list[Packet], port: Port) -> None: + """Send `packets` and block until they are fully sent. + + Send `packets` on `port`, then wait until `packets` are fully sent. + + Args: + packets: The packets to send. + port: The egress port on the TG node. + """ + self._logger.info(f"Sending packet{'s' if len(packets) > 1 else ''}.") + self._logger.debug(get_packet_summaries(packets)) + self._send_packets(packets, port) + + @abstractmethod + def _send_packets(self, packets: list[Packet], port: Port) -> None: + """The implementation of :method:`send_packets`. + + The subclasses must implement this method which sends `packets` on `port`. + The method should block until all `packets` are fully sent. + + What fully sent means is defined by the traffic generator. + """ + def send_packets_and_capture( self, packets: list[Packet], diff --git a/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py new file mode 100644 index 0000000000..7a384cf6e0 --- /dev/null +++ b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py @@ -0,0 +1,62 @@ +"""Performance testing capable traffic generatiors.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Callable + +from scapy.packet import Packet + +from framework.testbed_model.traffic_generator.traffic_generator import TrafficGenerator + + +@dataclass(slots=True) +class PerformanceTrafficStats(ABC): + """Data structure for stats offered by a given traffic generator.""" + + frame_size: int + + +class PerformanceTrafficGenerator(TrafficGenerator): + """An Abstract Base Class for all performance-oriented traffic generators. + + Provides an intermediary interface for performance-based traffic generator. + """ + + _test_stats: list[PerformanceTrafficStats] + + @property + def is_capturing(self) -> bool: + """Used for synchronization.""" + return False + + @property + def last_results(self) -> PerformanceTrafficStats | None: + """Get the latest set of results from TG instance. + + Returns: + The most recent set of traffic statistics. + """ + return self._test_stats.pop(0) + + def generate_traffic_and_stats( + self, + packet: Packet, + duration: float, # Default of 60 (in seconds). + ) -> PerformanceTrafficStats: + """Send packet traffic and acquire associated statistics.""" + return self._calculate_traffic_stats(packet, duration, self._generate_traffic) + + def setup(self, ports): + """Preliminary port setup prior to TG execution.""" + for port in self._tg_node.ports: + self._tg_node.main_session.configure_port_mtu(2000, port) + + @abstractmethod + def _calculate_traffic_stats( + self, packet: Packet, duration: float, traffic_gen_callback: Callable[[Packet, float], str] + ) -> PerformanceTrafficStats: + """Calculate packet traffic stats based on TG output.""" + + @abstractmethod + def _generate_traffic(self, packet: Packet, duration: float) -> str: + """Implementation for :method:`generate_traffic_and_stats`.""" diff --git a/dts/framework/testbed_model/traffic_generator/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py index 804662e114..12b9568d1a 100644 --- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py +++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py @@ -11,13 +11,11 @@ from abc import ABC, abstractmethod from typing import Iterable -from scapy.packet import Packet - from framework.config.test_run import TrafficGeneratorConfig from framework.logger import DTSLogger, get_dts_logger from framework.testbed_model.node import Node from framework.testbed_model.port import Port -from framework.utils import MultiInheritanceBaseClass, get_packet_summaries +from framework.utils import MultiInheritanceBaseClass class TrafficGenerator(MultiInheritanceBaseClass, ABC): @@ -56,45 +54,6 @@ def setup(self, ports: Iterable[Port]): def teardown(self, ports: Iterable[Port]): """Teardown the traffic generator.""" - def send_packet(self, packet: Packet, port: Port) -> None: - """Send `packet` and block until it is fully sent. - - Send `packet` on `port`, then wait until `packet` is fully sent. - - Args: - packet: The packet to send. - port: The egress port on the TG node. - """ - self.send_packets([packet], port) - - def send_packets(self, packets: list[Packet], port: Port) -> None: - """Send `packets` and block until they are fully sent. - - Send `packets` on `port`, then wait until `packets` are fully sent. - - Args: - packets: The packets to send. - port: The egress port on the TG node. - """ - self._logger.info(f"Sending packet{'s' if len(packets) > 1 else ''}.") - self._logger.debug(get_packet_summaries(packets)) - self._send_packets(packets, port) - - @abstractmethod - def _send_packets(self, packets: list[Packet], port: Port) -> None: - """The implementation of :method:`send_packets`. - - The subclasses must implement this method which sends `packets` on `port`. - The method should block until all `packets` are fully sent. - - What fully sent means is defined by the traffic generator. - """ - - @property - def is_capturing(self) -> bool: - """This traffic generator can't capture traffic.""" - return False - @abstractmethod def close(self) -> None: """Free all resources used by the traffic generator.""" -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC Patch v1 2/5] dts: rework traffic generator inheritance structure. 2025-04-23 19:40 ` [RFC Patch v1 2/5] dts: rework traffic generator inheritance structure Nicholas Pratte @ 2025-05-15 19:24 ` Patrick Robb 2025-05-16 19:12 ` Nicholas Pratte 0 siblings, 1 reply; 35+ messages in thread From: Patrick Robb @ 2025-05-15 19:24 UTC (permalink / raw) To: Nicholas Pratte Cc: ian.stokes, yoan.picchi, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen, dev [-- Attachment #1: Type: text/plain, Size: 3057 bytes --] On Wed, Apr 23, 2025 at 3:40 PM Nicholas Pratte <npratte@iol.unh.edu> wrote: > > + > def send_packets_and_capture( > self, > packets: list[Packet], > diff --git > a/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py > b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py > new file mode 100644 > index 0000000000..7a384cf6e0 > --- /dev/null > +++ > b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py > @@ -0,0 +1,62 @@ > +"""Performance testing capable traffic generatiors.""" > + > +from abc import ABC, abstractmethod > +from dataclasses import dataclass > +from typing import Callable > + > +from scapy.packet import Packet > + > +from framework.testbed_model.traffic_generator.traffic_generator import > TrafficGenerator > I think this can become: from .traffic_generator import TrafficGenerator > + > + > +@dataclass(slots=True) > +class PerformanceTrafficStats(ABC): > + """Data structure for stats offered by a given traffic generator.""" > + > + frame_size: int > Do we need to add an optional number of packet descriptors attribute? I realize that is a SUT testpmd param, not a TREX param, but presumably when we gather stats, we want to store the descriptor count with it. > + > + > +class PerformanceTrafficGenerator(TrafficGenerator): > + """An Abstract Base Class for all performance-oriented traffic > generators. > + > + Provides an intermediary interface for performance-based traffic > generator. > + """ > + > + _test_stats: list[PerformanceTrafficStats] > + > + @property > + def is_capturing(self) -> bool: > + """Used for synchronization.""" > + return False > + > + @property > + def last_results(self) -> PerformanceTrafficStats | None: > + """Get the latest set of results from TG instance. > + > + Returns: > + The most recent set of traffic statistics. > + """ > + return self._test_stats.pop(0) > + > + def generate_traffic_and_stats( > + self, > + packet: Packet, > + duration: float, # Default of 60 (in seconds). > Should it be float = 60, so the default is coming from the function, and not a "default" which is coming from the testsuite? If not, I guess the "default" comment belongs in the testsuite? > + ) -> PerformanceTrafficStats: > + """Send packet traffic and acquire associated statistics.""" > + return self._calculate_traffic_stats(packet, duration, > self._generate_traffic) > + > + def setup(self, ports): > + """Preliminary port setup prior to TG execution.""" > + for port in self._tg_node.ports: > + self._tg_node.main_session.configure_port_mtu(2000, port) > Okay just checking... so if we do not do this, even after binding the port to vfio-pci we cannot send traffic up to 2000 bytes? Reviewed-by: Patrick Robb <probb@iol.unh.edu> [-- Attachment #2: Type: text/html, Size: 4419 bytes --] ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC Patch v1 2/5] dts: rework traffic generator inheritance structure. 2025-05-15 19:24 ` Patrick Robb @ 2025-05-16 19:12 ` Nicholas Pratte 0 siblings, 0 replies; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 19:12 UTC (permalink / raw) To: Patrick Robb Cc: yoan.picchi, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen, dev Hi Patick, see below! >> + >> +from framework.testbed_model.traffic_generator.traffic_generator import TrafficGenerator > > > I think this can become: > > from .traffic_generator import TrafficGenerator Ack. > >> >> + >> + >> +@dataclass(slots=True) >> +class PerformanceTrafficStats(ABC): >> + """Data structure for stats offered by a given traffic generator.""" >> + >> + frame_size: int > > > Do we need to add an optional number of packet descriptors attribute? I realize that is a SUT testpmd param, not a TREX param, but presumably when we gather stats, we want to store the descriptor count with it.' It could be done by passing another parameter when the function is called. Maybe adding these parameters is helpful, maybe it isn't. It might just be easier to strip the frame size and descriptors away entirely from the statistics data structure, under the assumption that these fields would be managed in the test suites themselves. I think either/or would be fine. > >> >> + >> + >> +class PerformanceTrafficGenerator(TrafficGenerator): >> + """An Abstract Base Class for all performance-oriented traffic generators. >> + >> + Provides an intermediary interface for performance-based traffic generator. >> + """ >> + >> + _test_stats: list[PerformanceTrafficStats] >> + >> + @property >> + def is_capturing(self) -> bool: >> + """Used for synchronization.""" >> + return False >> + >> + @property >> + def last_results(self) -> PerformanceTrafficStats | None: >> + """Get the latest set of results from TG instance. >> + >> + Returns: >> + The most recent set of traffic statistics. >> + """ >> + return self._test_stats.pop(0) >> + >> + def generate_traffic_and_stats( >> + self, >> + packet: Packet, >> + duration: float, # Default of 60 (in seconds). > > > Should it be float = 60, so the default is coming from the function, and not a "default" which is coming from the testsuite? If not, I guess the "default" comment belongs in the testsuite? I would say the comment can be removed. Right now, as you mentioned, it is set up so that the default value is asserted at the test suite level. It might just make sense to do away with this default value and just insist the user explicitly provide a duration. That would make sense in its own right. ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC Patch v1 3/5] dts: add asychronous support to ssh sessions. 2025-04-23 19:40 [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 1/5] dts: rework config module to support perf TGs Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 2/5] dts: rework traffic generator inheritance structure Nicholas Pratte @ 2025-04-23 19:40 ` Nicholas Pratte 2025-05-15 19:24 ` Patrick Robb 2025-04-23 19:40 ` [RFC Patch v1 4/5] dts: add trex traffic generator to dts framework Nicholas Pratte ` (3 subsequent siblings) 6 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-04-23 19:40 UTC (permalink / raw) To: ian.stokes, yoan.picchi, probb, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen Cc: dev, Nicholas Pratte Execution of the TREX server process requires an SSH session rework to support asynchronous process management. Allowing access to asynchronous functionality allows developers to execute processes without hijacking the SSH session being used. In doing so, both timeout and runtime errors may be avoided. This functionality leverages Fabric's Promise class, which provides a join method to terminate the process when the process is done being used, providing more secure process control. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/framework/remote_session/ssh_session.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dts/framework/remote_session/ssh_session.py b/dts/framework/remote_session/ssh_session.py index e6e4704bc2..185905f701 100644 --- a/dts/framework/remote_session/ssh_session.py +++ b/dts/framework/remote_session/ssh_session.py @@ -13,6 +13,7 @@ ThreadException, UnexpectedExit, ) +from invoke.runners import Promise from paramiko.ssh_exception import ( AuthenticationException, BadHostKeyException, @@ -99,6 +100,22 @@ def _send_command(self, command: str, timeout: float, env: dict | None) -> Comma return CommandResult(self.name, command, output.stdout, output.stderr, output.return_code) + def _send_async_command(self, command: str, timeout: float, env: dict | None) -> Promise: + try: + promise = self.session.run( + command, env=env, warn=True, hide=True, timeout=timeout, asynchronous=True + ) + + except (UnexpectedExit, ThreadException) as e: + self._logger.exception(e) + raise SSHSessionDeadError(self.hostname) from e + + except CommandTimedOut as e: + self._logger.exception(e) + raise SSHTimeoutError(command) from e + + return promise + def is_alive(self) -> bool: """Overrides :meth:`~.remote_session.RemoteSession.is_alive`.""" return self.session.is_connected -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC Patch v1 3/5] dts: add asychronous support to ssh sessions. 2025-04-23 19:40 ` [RFC Patch v1 3/5] dts: add asychronous support to ssh sessions Nicholas Pratte @ 2025-05-15 19:24 ` Patrick Robb 0 siblings, 0 replies; 35+ messages in thread From: Patrick Robb @ 2025-05-15 19:24 UTC (permalink / raw) To: Nicholas Pratte Cc: ian.stokes, yoan.picchi, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen, dev [-- Attachment #1: Type: text/plain, Size: 46 bytes --] Reviewed-by: Patrick Robb <probb@iol.unh.edu> [-- Attachment #2: Type: text/html, Size: 112 bytes --] ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC Patch v1 4/5] dts: add trex traffic generator to dts framework 2025-04-23 19:40 [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework Nicholas Pratte ` (2 preceding siblings ...) 2025-04-23 19:40 ` [RFC Patch v1 3/5] dts: add asychronous support to ssh sessions Nicholas Pratte @ 2025-04-23 19:40 ` Nicholas Pratte 2025-05-15 19:25 ` Patrick Robb 2025-04-23 19:40 ` [RFC Patch v1 5/5] dts: add performance test functions to test suite api Nicholas Pratte ` (2 subsequent siblings) 6 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-04-23 19:40 UTC (permalink / raw) To: ian.stokes, yoan.picchi, probb, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen Cc: dev, Nicholas Pratte Implement the TREX traffic generator for use in the DTS framework. The provided implementation leverages TREX's stateless API automation library, via use of a Python shell. As such, version control of TREX may be needed. The DTS context has been modified to include a performance traffic generator in addition to a functional traffic generator. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/framework/config/test_run.py | 20 +- dts/framework/context.py | 11 +- dts/framework/test_run.py | 28 +- dts/framework/test_suite.py | 6 +- .../traffic_generator/__init__.py | 22 +- .../testbed_model/traffic_generator/trex.py | 287 ++++++++++++++++++ 6 files changed, 356 insertions(+), 18 deletions(-) create mode 100644 dts/framework/testbed_model/traffic_generator/trex.py diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py index 06fe28143c..5cdd391b49 100644 --- a/dts/framework/config/test_run.py +++ b/dts/framework/config/test_run.py @@ -393,6 +393,8 @@ class TrafficGeneratorType(str, Enum): #: SCAPY = "SCAPY" + #: + TREX = "TREX" class TrafficGeneratorConfig(FrozenModel): @@ -401,6 +403,8 @@ class TrafficGeneratorConfig(FrozenModel): #: The traffic generator type the child class is required to define to be distinguished among #: others. type: TrafficGeneratorType + remote_path: PurePath + config: PurePath class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): @@ -409,8 +413,16 @@ class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): type: Literal[TrafficGeneratorType.SCAPY] +class TrexTrafficGeneratorConfig(TrafficGeneratorConfig): + """TREX traffic generator specific configuration.""" + + type: Literal[TrafficGeneratorType.TREX] + + #: A union type discriminating traffic generators by the `type` field. -TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")] +TrafficGeneratorConfigTypes = Annotated[ + TrexTrafficGeneratorConfig, ScapyTrafficGeneratorConfig, Field(discriminator="type") +] #: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores. LogicalCores = Annotated[ @@ -458,8 +470,10 @@ class TestRunConfiguration(FrozenModel): #: The DPDK configuration used to test. dpdk: DPDKConfiguration - #: The traffic generator configuration used to test. - traffic_generator: TrafficGeneratorConfigTypes + #: The traffic generator configuration used for functional tests. + func_traffic_generator: TrafficGeneratorConfig + #: The traffic generator configuration used for performance tests. + perf_traffic_generator: TrafficGeneratorConfig #: Whether to run performance tests. perf: bool #: Whether to run functional tests. diff --git a/dts/framework/context.py b/dts/framework/context.py index ddd7ed4d36..0a83e2b269 100644 --- a/dts/framework/context.py +++ b/dts/framework/context.py @@ -15,7 +15,12 @@ if TYPE_CHECKING: from framework.remote_session.dpdk import DPDKBuildEnvironment, DPDKRuntimeEnvironment - from framework.testbed_model.traffic_generator.traffic_generator import TrafficGenerator + from framework.testbed_model.traffic_generator.capturing_traffic_generator import ( + CapturingTrafficGenerator, + ) + from framework.testbed_model.traffic_generator.performance_traffic_generator import ( + PerformanceTrafficGenerator, + ) P = ParamSpec("P") @@ -68,7 +73,9 @@ class Context: topology: Topology dpdk_build: "DPDKBuildEnvironment" dpdk: "DPDKRuntimeEnvironment" - tg: "TrafficGenerator" + tg_dpdk: "DPDKRuntimeEnvironment" | None + func_tg: "CapturingTrafficGenerator" + perf_tg: "PerformanceTrafficGenerator" local: LocalContext = field(default_factory=LocalContext) diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py index f9cfe5e908..81dc553b00 100644 --- a/dts/framework/test_run.py +++ b/dts/framework/test_run.py @@ -107,7 +107,7 @@ from types import MethodType from typing import ClassVar, Protocol, Union -from framework.config.test_run import TestRunConfiguration +from framework.config.test_run import TestRunConfiguration, TrafficGeneratorType from framework.context import Context, init_ctx from framework.exception import ( InternalError, @@ -204,10 +204,25 @@ def __init__( dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node) dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env) - traffic_generator = create_traffic_generator(config.traffic_generator, tg_node) + # There is definitely a better way to do this. + tg_dpdk_runtime_env = None + if ( + config.perf_traffic_generator.type == TrafficGeneratorType.TREX + or config.func_traffic_generator.type == TrafficGeneratorType.TREX + ): + tg_dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, tg_node, None) + func_traffic_generator = create_traffic_generator(config.func_traffic_generator, tg_node) + perf_traffic_generator = create_traffic_generator(config.perf_traffic_generator, tg_node) self.ctx = Context( - sut_node, tg_node, topology, dpdk_build_env, dpdk_runtime_env, traffic_generator + sut_node, + tg_node, + topology, + dpdk_build_env, + dpdk_runtime_env, + tg_dpdk_runtime_env, + func_traffic_generator, + perf_traffic_generator, ) self.result = result self.selected_tests = list(self.config.filter_tests(tests_config)) @@ -345,7 +360,9 @@ def next(self) -> State | None: test_run.ctx.sut_node.setup() test_run.ctx.tg_node.setup() test_run.ctx.dpdk.setup(test_run.ctx.topology.sut_ports) - test_run.ctx.tg.setup(test_run.ctx.topology.tg_ports) + test_run.ctx.tg_dpdk.setup(test_run.ctx.topology.tg_ports) + test_run.ctx.func_tg.setup(test_run.ctx.topology.tg_ports) + test_run.ctx.perf_tg.setup(test_run.ctx.topology.tg_ports) self.result.ports = test_run.ctx.topology.sut_ports + test_run.ctx.topology.tg_ports self.result.sut_info = test_run.ctx.sut_node.node_info @@ -430,7 +447,8 @@ def description(self) -> str: def next(self) -> State | None: """Next state.""" - self.test_run.ctx.tg.teardown(self.test_run.ctx.topology.tg_ports) + self.test_run.ctx.func_tg.teardown(self.test_run.ctx.topology.tg_ports) + self.test_run.ctx.perf_tg.teardown(self.test_run.ctx.topology.tg_ports) self.test_run.ctx.dpdk.teardown(self.test_run.ctx.topology.sut_ports) self.test_run.ctx.tg_node.teardown() self.test_run.ctx.sut_node.teardown() diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py index e07c327b77..507df508cb 100644 --- a/dts/framework/test_suite.py +++ b/dts/framework/test_suite.py @@ -254,11 +254,11 @@ def send_packets_and_capture( A list of received packets. """ assert isinstance( - self._ctx.tg, CapturingTrafficGenerator + self._ctx.func_tg, CapturingTrafficGenerator ), "Cannot capture with a non-capturing traffic generator" # TODO: implement @requires for types of traffic generator packets = self._adjust_addresses(packets) - return self._ctx.tg.send_packets_and_capture( + return self._ctx.func_tg.send_packets_and_capture( packets, self._ctx.topology.tg_port_egress, self._ctx.topology.tg_port_ingress, @@ -276,7 +276,7 @@ def send_packets( packets: Packets to send. """ packets = self._adjust_addresses(packets) - self._ctx.tg.send_packets(packets, self._ctx.topology.tg_port_egress) + self._ctx.func_tg.send_packets(packets, self._ctx.topology.tg_port_egress) def get_expected_packets( self, diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py index 2a259a6e6c..53125995cd 100644 --- a/dts/framework/testbed_model/traffic_generator/__init__.py +++ b/dts/framework/testbed_model/traffic_generator/__init__.py @@ -14,17 +14,27 @@ and a capturing traffic generator is required. """ -from framework.config.test_run import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig +from framework.config.test_run import ( + ScapyTrafficGeneratorConfig as ScapyTrafficGeneratorConfig, +) +from framework.config.test_run import ( + TrafficGeneratorConfig, + TrafficGeneratorType, +) +from framework.config.test_run import ( + TrexTrafficGeneratorConfig as TrexTrafficGeneratorConfig, +) from framework.exception import ConfigurationError from framework.testbed_model.node import Node -from .capturing_traffic_generator import CapturingTrafficGenerator from .scapy import ScapyTrafficGenerator +from .traffic_generator import TrafficGenerator +from .trex import TrexTrafficGenerator def create_traffic_generator( traffic_generator_config: TrafficGeneratorConfig, node: Node -) -> CapturingTrafficGenerator: +) -> TrafficGenerator: """The factory function for creating traffic generator objects from the test run configuration. Args: @@ -37,8 +47,10 @@ def create_traffic_generator( Raises: ConfigurationError: If an unknown traffic generator has been setup. """ - match traffic_generator_config: - case ScapyTrafficGeneratorConfig(): + match traffic_generator_config.type: + case TrafficGeneratorType.SCAPY: return ScapyTrafficGenerator(node, traffic_generator_config, privileged=True) + case TrafficGeneratorType.TREX: + return TrexTrafficGenerator(node, traffic_generator_config, privileged=True) case _: raise ConfigurationError(f"Unknown traffic generator: {traffic_generator_config.type}") diff --git a/dts/framework/testbed_model/traffic_generator/trex.py b/dts/framework/testbed_model/traffic_generator/trex.py new file mode 100644 index 0000000000..0053174ede --- /dev/null +++ b/dts/framework/testbed_model/traffic_generator/trex.py @@ -0,0 +1,287 @@ +"""Implementation for TREX performance traffic generator.""" + +import time +from dataclasses import dataclass +from enum import Flag, auto +from typing import Callable, ClassVar + +from invoke.runners import Promise +from scapy.packet import Packet + +from framework.config.node import NodeConfiguration +from framework.config.test_run import TrafficGeneratorConfig +from framework.exception import SSHTimeoutError +from framework.remote_session.python_shell import PythonShell +from framework.remote_session.ssh_session import SSHSession +from framework.testbed_model.linux_session import LinuxSession +from framework.testbed_model.node import Node, create_session +from framework.testbed_model.traffic_generator.performance_traffic_generator import ( + PerformanceTrafficGenerator, + PerformanceTrafficStats, +) + + +@dataclass +class TrexPerPortStats: + """Performance statistics on a per port basis. + + Attributes: + opackets: Number of packets sent. + obytes: Number of egress bytes sent. + tx_bps: Maximum bits per second transmitted. + tx_pps: Number of transmitted packets sent. + """ + + opackets: float + obytes: float + tx_bps: float + tx_pps: float + + +@dataclass +class TrexPerformanceStats(PerformanceTrafficStats): + """Data structure to store performance statistics for a given test run. + + Attributes: + packet: The packet that was sent in the test run. + frame_size: The total length of the frame. (L2 downward) + tx_expected_bps: The expected bits per second on a given NIC. + tx_expected_cps: ... + tx_expected_pps: The expected packets per second of a given NIC. + tx_pps: The recorded maximum packets per second of the tested NIC. + tx_cps: The recorded maximum cps of the tested NIC + tx_bps: The recorded maximum bits per second of the tested NIC. + obytes: Total bytes output during test run. + port_stats: A list of :class:`TrexPerPortStats` provided by TREX. + """ + + packet: Packet + frame_size: int + + tx_expected_bps: float + tx_expected_cps: float + tx_expected_pps: float + + tx_pps: float + tx_cps: float + tx_bps: float + + obytes: float + + port_stats: list[TrexPerPortStats] | None + + +class TrexStatelessTXModes(Flag): + """Flags indicating TREX instance's current trasmission mode.""" + + CONTINUOUS = auto() + SINGLE_BURST = auto() + MULTI_BURST = auto() + + +class TrexTrafficGenerator(PythonShell, PerformanceTrafficGenerator): + """TREX traffic generator. + + This implementation leverages the stateless API library provided in the TREX installation. + + Attributes: + stl_client_name: The name of the stateless client used in the stateless API. + packet_stream_name: The name of the stateless packet stream used in the stateless API. + timeout_duration: Internal timeout for connection to the TREX server. + """ + + _os_session: LinuxSession + _server_remote_session: SSHSession + _trex_server_process: Promise + + _tg_config: TrafficGeneratorConfig + _node_config: NodeConfiguration + + _python_indentation: ClassVar[str] = " " * 4 + + stl_client_name: ClassVar[str] = "client" + packet_stream_name: ClassVar[str] = "stream" + + _streaming_mode: TrexStatelessTXModes = TrexStatelessTXModes.CONTINUOUS + + timeout_duration: int + + def __init__( + self, tg_node: Node, config: TrafficGeneratorConfig, timeout_duration: int = 5, **kwargs + ) -> None: + """Initialize the TREX server. + + Initializes needed OS sessions for the creation of the TREX server process. + + Attributes: + tg_node: TG node the TREX instance is operating on. + config: Traffic generator config provided for TREX instance. + timeout_duration: Internal timeout for connection to the TREX server. + """ + super().__init__(node=tg_node, config=config, tg_node=tg_node, **kwargs) + self._node_config = tg_node.config + self._tg_config = config + self.timeout_duration = timeout_duration + + # Create TREX server session. + self._tg_node._other_sessions.append( + create_session(self._tg_node.config, "TREX Server.", self._logger) + ) + self._os_session = self._tg_node._other_sessions[0] + self._server_remote_session = self._os_session.remote_session + + def setup(self, ports): + """Initialize and start a TREX server process. + + Binds TG ports to vfio-pci and starts the trex process. + + Attributes: + ports: Related ports utilized in TG instance. + """ + super().setup(ports) + # Start TREX server process. + try: + self._logger.info("Starting TREX server process: sending 45 second sleep.") + privileged_command = self._os_session._get_privileged_command( + f""" + cd /opt/v3.03/; {self._tg_config.remote_path}/t-rex-64 + --cfg {self._tg_config.config} -i + """ + ) + self._server_remote_session = self._server_remote_session._send_async_command( + privileged_command, timeout=None, env=None + ) + time.sleep(45) + except SSHTimeoutError as e: + self._logger.exception("Failed to start TREX server process.", e) + + # Start Python shell. + self.start_application() + self.send_command("import os") + # Parent directory: /opt/v3.03/automation/trex_control_plane/interactive + self.send_command( + f"os.chdir('{self._tg_config.remote_path}/automation/trex_control_plane/interactive')" + ) + + # Import stateless API components. + imports = [ + "import trex", + "import trex.stl", + "import trex.stl.trex_stl_client", + "import trex.stl.trex_stl_streams", + "import trex.stl.trex_stl_packet_builder_scapy", + "from scapy.layers.l2 import Ether", + "from scapy.layers.inet import IP", + "from scapy.packet import Raw", + ] + self.send_command("\n".join(imports)) + + stateless_client = [ + f"{self.stl_client_name} = trex.stl.trex_stl_client.STLClient(", + f"username='{self._node_config.user}',", + "server='127.0.0.1',", + f"sync_timeout={self.timeout_duration}", + ")", + ] + self.send_command(f"\n{self._python_indentation}".join(stateless_client)) + self.send_command(f"{self.stl_client_name}.connect()") + + def teardown(self, ports): + """Teardown the TREX server and stateless implementation. + + close the TREX server process, and stop the Python shell. + + Attributes: + ports: Associated ports used by the TREX instance. + """ + super().teardown(ports) + self.send_command(f"{self.stl_client_name}.disconnect()") + self.close() + self._trex_server_process.join() + + def _calculate_traffic_stats( + self, packet: Packet, duration: float, callback: Callable[[Packet, float], str] + ) -> PerformanceTrafficStats: + """Calculate the traffic statistics, using provided TG output. + + Takes in the statistics output provided by the stateless API implementation, and collects + them into a performance statistics data structure. + + Attributes: + packet: The packet being used for the performance test. + duration: The duration of the test. + callback: The callback function used to generate the traffic. + """ + # Convert to a dictionary. + stats_output = eval(callback(packet, duration)) + return TrexPerformanceStats( + len(packet), + packet, + stats_output.get("tx_expected_bps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_expected_cps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_expected_pps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_pps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_cps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_bps", "ERROR - DATA NOT FOUND"), + stats_output.get("obytes", "ERROR - DATA NOT FOUND"), + None, + ) + + def set_streaming_mode(self, streaming_mode: TrexStatelessTXModes) -> None: + """Set the streaming mode of the TREX instance.""" + # Streaming modes are mutually exclusive. + self._streaming_mode = self._streaming_mode & streaming_mode + + def _generate_traffic(self, packet: Packet, duration: float) -> str: + """Generate traffic using provided packet. + + Uses the provided packet to generate traffic for the provided duration. + + Attributes: + packet: The packet being used for the performance test. + duration: The duration of the test being performed. + + Returns: + a string output of statistics provided by the traffic generator. + """ + """Implementation for :method:`generate_traffic_and_stats`.""" + streaming_mode = "" + if self._streaming_mode == TrexStatelessTXModes.CONTINUOUS: + streaming_mode = "STLTXCont" + elif self._streaming_mode == TrexStatelessTXModes.SINGLE_BURST: + streaming_mode = "STLTXSingleBurst" + elif self._streaming_mode == TrexStatelessTXModes.MULTI_BURST: + streaming_mode = "STLTXMultiBurst" + + packet_stream = [ + f"{self.packet_stream_name} = trex.stl.trex_stl_streams.STLStream(", + f"name='Test_{len(packet)}_bytes',", + f"packet=trex.stl.trex_stl_packet_builder_scapy.STLPktBuilder(pkt={packet.command()}),", + f"mode=trex.stl.trex_stl_streams.{streaming_mode}(),", + ")", + ] + self.send_command("\n".join(packet_stream)) + + # Prepare TREX console for next performance test. + procedure = [ + f"{self.stl_client_name}.connect()", + f"{self.stl_client_name}.reset(ports = [0, 1])", + f"{self.stl_client_name}.add_streams({self.packet_stream_name}, ports=[0, 1])", + f"{self.stl_client_name}.clear_stats()", + ")", + ] + self.send_command("\n".join(procedure)) + + start_test = [ + f"{self.stl_client_name}.start(ports=[0, 1], duration={duration})", + f"{self.stl_client_name}.wait_on_traffic(ports=[0, 1])", + ] + self.send_command("\n".join(start_test)) + import time + + time.sleep(duration + 1) + + # Gather statistics output for parsing. + return self.send_command( + f"{self.stl_client_name}.get_stats(ports=[0, 1])", skip_first_line=True + ) -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC Patch v1 4/5] dts: add trex traffic generator to dts framework 2025-04-23 19:40 ` [RFC Patch v1 4/5] dts: add trex traffic generator to dts framework Nicholas Pratte @ 2025-05-15 19:25 ` Patrick Robb 2025-05-16 19:45 ` Nicholas Pratte 0 siblings, 1 reply; 35+ messages in thread From: Patrick Robb @ 2025-05-15 19:25 UTC (permalink / raw) To: Nicholas Pratte Cc: ian.stokes, yoan.picchi, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen, dev [-- Attachment #1: Type: text/plain, Size: 2921 bytes --] On Wed, Apr 23, 2025 at 3:40 PM Nicholas Pratte <npratte@iol.unh.edu> wrote: > Implement the TREX traffic generator for use in the DTS framework. The > provided implementation leverages TREX's stateless API automation > library, via use of a Python shell. As such, version control of TREX may > be needed. The DTS context has been modified to include a performance > traffic generator in addition to a functional traffic generator. > I think the statement is that only certain versions are confirmed to work off of your implementation? I think usage of the term version control is confusing since we are just talking about using particular versions of TREX, and not using a "version control" tool like git. This is more about listing approved versions of the dependency for the TG. > > > dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node) > dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, > dpdk_build_env) > - traffic_generator = > create_traffic_generator(config.traffic_generator, tg_node) > + # There is definitely a better way to do this. > + tg_dpdk_runtime_env = None > + if ( > + config.perf_traffic_generator.type == > TrafficGeneratorType.TREX > + or config.func_traffic_generator.type == > TrafficGeneratorType.TREX > + ): > We have this from the testrun config perf: false # disable performance testing func: true # enable functional testing So, use if testrunconfig.perf == true? > > + > +@dataclass > +class TrexPerformanceStats(PerformanceTrafficStats): > + """Data structure to store performance statistics for a given test > run. > + > + Attributes: > + packet: The packet that was sent in the test run. > + frame_size: The total length of the frame. (L2 downward) > + tx_expected_bps: The expected bits per second on a given NIC. > + tx_expected_cps: ... > "The expected connections per second" ? I think you just missed this one. > + > + Attributes: > + ports: Related ports utilized in TG instance. > + """ > + super().setup(ports) > + # Start TREX server process. > + try: > + self._logger.info("Starting TREX server process: sending 45 > second sleep.") > + privileged_command = self._os_session._get_privileged_command( > + f""" > + cd /opt/v3.03/; {self._tg_config.remote_path}/t-rex-64 > + --cfg {self._tg_config.config} -i > + """ > I know this was just a work in progress for RFC with some hardcoding, but you already have tg_config.remote_path, right? So, the leading hardcoded cd does not do anything, and even if you had to cd there you could use the tg_config.remote_path? Reviewed-by: Patrick Robb <probb@iol.unh.edu> [-- Attachment #2: Type: text/html, Size: 4200 bytes --] ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC Patch v1 4/5] dts: add trex traffic generator to dts framework 2025-05-15 19:25 ` Patrick Robb @ 2025-05-16 19:45 ` Nicholas Pratte 0 siblings, 0 replies; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 19:45 UTC (permalink / raw) To: Patrick Robb Cc: ian.stokes, yoan.picchi, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen, dev >> Implement the TREX traffic generator for use in the DTS framework. The >> provided implementation leverages TREX's stateless API automation >> library, via use of a Python shell. As such, version control of TREX may >> be needed. The DTS context has been modified to include a performance >> traffic generator in addition to a functional traffic generator. > > > I think the statement is that only certain versions are confirmed to work off of your implementation? I think usage of the term version control is confusing since we are just talking about using particular versions of TREX, and not using a "version control" tool like git. This is more about listing approved versions of the dependency for the TG. I can fix that! >> >> >> >> dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node) >> dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env) >> - traffic_generator = create_traffic_generator(config.traffic_generator, tg_node) >> + # There is definitely a better way to do this. >> + tg_dpdk_runtime_env = None >> + if ( >> + config.perf_traffic_generator.type == TrafficGeneratorType.TREX >> + or config.func_traffic_generator.type == TrafficGeneratorType.TREX >> + ): > > > We have this from the testrun config > > perf: false # disable performance testing > func: true # enable functional testing > > So, use if testrunconfig.perf == true? I see what you're saying, I can make that change for the time being. My reason for explicitly checking traffic generators types was that any future implementations may not need a DPDK runtime environment. >> >> >> + >> +@dataclass >> +class TrexPerformanceStats(PerformanceTrafficStats): >> + """Data structure to store performance statistics for a given test run. >> + >> + Attributes: >> + packet: The packet that was sent in the test run. >> + frame_size: The total length of the frame. (L2 downward) >> + tx_expected_bps: The expected bits per second on a given NIC. >> + tx_expected_cps: ... > > > "The expected connections per second" ? I think you just missed this one. This was ultimately removed from the final version, incidentally. But the way this was implemented does allow us to check this data. > >> >> + >> + Attributes: >> + ports: Related ports utilized in TG instance. >> + """ >> + super().setup(ports) >> + # Start TREX server process. >> + try: >> + self._logger.info("Starting TREX server process: sending 45 second sleep.") >> + privileged_command = self._os_session._get_privileged_command( >> + f""" >> + cd /opt/v3.03/; {self._tg_config.remote_path}/t-rex-64 >> + --cfg {self._tg_config.config} -i >> + """ > > > I know this was just a work in progress for RFC with some hardcoding, but you already have tg_config.remote_path, right? So, the leading hardcoded cd does not do anything, and even if you had to cd there you could use the tg_config.remote_path? Yes, that was hard coded as a temporary measure. The cd is needed to ensure that the TREX executable actually runs properly. I must have just forgot to switch that back as I was working with haste. ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC Patch v1 5/5] dts: add performance test functions to test suite api 2025-04-23 19:40 [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework Nicholas Pratte ` (3 preceding siblings ...) 2025-04-23 19:40 ` [RFC Patch v1 4/5] dts: add trex traffic generator to dts framework Nicholas Pratte @ 2025-04-23 19:40 ` Nicholas Pratte 2025-05-15 19:25 ` Patrick Robb 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte 2025-07-02 5:21 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Patrick Robb 6 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-04-23 19:40 UTC (permalink / raw) To: ian.stokes, yoan.picchi, probb, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen Cc: dev, Nicholas Pratte Provide functional performance method to run performance tests using a user-supplied performance traffic generator. The single core performance test is included, with some basic statistics checks verifying TG packet transmission rates. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/framework/test_suite.py | 27 +++++++++++++++++++++++++ dts/tests/TestSuite_single_core_perf.py | 24 ++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 dts/tests/TestSuite_single_core_perf.py diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py index 507df508cb..a89faac2d5 100644 --- a/dts/framework/test_suite.py +++ b/dts/framework/test_suite.py @@ -38,6 +38,10 @@ CapturingTrafficGenerator, PacketFilteringConfig, ) +from framework.testbed_model.traffic_generator.performance_traffic_generator import ( + PerformanceTrafficGenerator, + PerformanceTrafficStats, +) from .exception import ConfigurationError, InternalError, TestCaseVerifyError from .logger import DTSLogger, get_dts_logger @@ -266,6 +270,26 @@ def send_packets_and_capture( duration, ) + def assess_performance_by_packet( + self, packet: Packet, duration: int = 60 + ) -> PerformanceTrafficStats: + """Send a given packet for a given duration and assess basic performance statistics. + + Send `packet` and assess NIC performance for a given duration, corresponding to the test + suite's given topology. + + Args: + packet: The packet to send. + duration: Performance test duration (in seconds) + + Returns: + Performance statistics of the generated test. + """ + assert isinstance( + self._ctx.perf_tg, PerformanceTrafficGenerator + ), "Cannot run performance tests on non-performance traffic generator." + return self._ctx.perf_tg.generate_traffic_and_stats(packet, duration) + def send_packets( self, packets: list[Packet], @@ -275,6 +299,9 @@ def send_packets( Args: packets: Packets to send. """ + assert isinstance( + self._ctx.perf_tg, CapturingTrafficGenerator + ), "Cannot run performance tests on non-capturing traffic generator." packets = self._adjust_addresses(packets) self._ctx.func_tg.send_packets(packets, self._ctx.topology.tg_port_egress) diff --git a/dts/tests/TestSuite_single_core_perf.py b/dts/tests/TestSuite_single_core_perf.py new file mode 100644 index 0000000000..31a8c8608f --- /dev/null +++ b/dts/tests/TestSuite_single_core_perf.py @@ -0,0 +1,24 @@ +"""Single core performance test suite.""" + +from scapy.layers.inet import IP +from scapy.layers.l2 import Ether +from scapy.packet import Raw + +from framework.remote_session.testpmd_shell import TestPmdShell +from framework.test_suite import TestSuite, perf_test + + +class TestSingleCorePerf(TestSuite): + """Single core performance test suite.""" + + @perf_test + def test_perf_test(self) -> None: + """Prototype test case.""" + with TestPmdShell() as testpmd: + packet = Ether() / IP() / Raw(load="x" * 1484) # 1518 byte packet. + + testpmd.start() + stats = self.assess_performance_by_packet(packet, duration=5) + self.verify( + stats.tx_expected_bps == 40, "Expected output does not patch recorded output." + ) -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC Patch v1 5/5] dts: add performance test functions to test suite api 2025-04-23 19:40 ` [RFC Patch v1 5/5] dts: add performance test functions to test suite api Nicholas Pratte @ 2025-05-15 19:25 ` Patrick Robb 0 siblings, 0 replies; 35+ messages in thread From: Patrick Robb @ 2025-05-15 19:25 UTC (permalink / raw) To: Nicholas Pratte Cc: ian.stokes, yoan.picchi, paul.szczepanek, Honnappa.Nagarahalli, thomas, luca.vizzarro, thomas.wilks, dmarx, stephen, dev [-- Attachment #1: Type: text/plain, Size: 491 bytes --] On Wed, Apr 23, 2025 at 3:40 PM Nicholas Pratte <npratte@iol.unh.edu> wrote: > Provide functional performance method to run performance tests using a > user-supplied performance traffic generator. The single core performance > test is included, with some basic statistics checks verifying TG packet > transmission rates. > > > Obviously the testsuite is a placeholder to demonstrate the TG usage, as you say, which is all good. Reviewed-by: Patrick Robb <probb@iol.unh.edu> [-- Attachment #2: Type: text/html, Size: 918 bytes --] ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework 2025-04-23 19:40 [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework Nicholas Pratte ` (4 preceding siblings ...) 2025-04-23 19:40 ` [RFC Patch v1 5/5] dts: add performance test functions to test suite api Nicholas Pratte @ 2025-05-16 20:18 ` Nicholas Pratte 2025-05-16 20:18 ` [RFC v2 1/6] dts: rework config module to support perf TGs Nicholas Pratte ` (5 more replies) 2025-07-02 5:21 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Patrick Robb 6 siblings, 6 replies; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 20:18 UTC (permalink / raw) To: stephen, dmarx, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Cc: dev, Nicholas Pratte v2: * Still some formatting issues that need clean up. * Several issues have been addressed - Includes some of Patrick's comments. - Personally identified bug fixes. * Single core perf test has been fleshed out. Nicholas Pratte (6): dts: rework config module to support perf TGs dts: rework traffic generator inheritance structure. dts: add asynchronous support to ssh sessions. dts: add extended timeout option to interactive shells. dts: add trex traffic generator to dts framework dts: add performance test functions to test suite api dts/{ => configurations}/nodes.example.yaml | 0 .../test_run.example.yaml | 8 +- dts/configurations/tests_config.example.yaml | 9 + .../trex_configs/intel_40g.yaml | 18 ++ dts/framework/config/test_run.py | 20 +- dts/framework/context.py | 11 +- .../remote_session/interactive_shell.py | 9 +- dts/framework/remote_session/ssh_session.py | 14 + dts/framework/settings.py | 6 +- dts/framework/test_run.py | 27 +- dts/framework/test_suite.py | 33 +- .../traffic_generator/__init__.py | 22 +- .../capturing_traffic_generator.py | 34 ++ .../performance_traffic_generator.py | 69 +++++ .../traffic_generator/traffic_generator.py | 43 --- .../testbed_model/traffic_generator/trex.py | 292 ++++++++++++++++++ dts/tests/TestSuite_single_core_perf.py | 56 ++++ dts/tests_config.example.yaml | 4 - 18 files changed, 605 insertions(+), 70 deletions(-) rename dts/{ => configurations}/nodes.example.yaml (100%) rename dts/{ => configurations}/test_run.example.yaml (82%) create mode 100644 dts/configurations/tests_config.example.yaml create mode 100644 dts/configurations/trex_configs/intel_40g.yaml create mode 100644 dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py create mode 100644 dts/framework/testbed_model/traffic_generator/trex.py create mode 100644 dts/tests/TestSuite_single_core_perf.py delete mode 100644 dts/tests_config.example.yaml -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC v2 1/6] dts: rework config module to support perf TGs 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte @ 2025-05-16 20:18 ` Nicholas Pratte 2025-05-20 20:33 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 2/6] dts: rework traffic generator inheritance structure Nicholas Pratte ` (4 subsequent siblings) 5 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 20:18 UTC (permalink / raw) To: stephen, dmarx, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Cc: dev, Nicholas Pratte Rework test run configuration file for TGs to support both application directory location and any necessary configuration files; an example TREX configuration file is provided. Configuration files have been moved to a configurations directory, requiring a slight modification to the settings module. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/{ => configurations}/nodes.example.yaml | 0 dts/{ => configurations}/test_run.example.yaml | 8 +++++++- .../tests_config.example.yaml | 0 dts/configurations/trex_configs/intel_40g.yaml | 18 ++++++++++++++++++ dts/framework/settings.py | 6 ++++-- 5 files changed, 29 insertions(+), 3 deletions(-) rename dts/{ => configurations}/nodes.example.yaml (100%) rename dts/{ => configurations}/test_run.example.yaml (82%) rename dts/{ => configurations}/tests_config.example.yaml (100%) create mode 100644 dts/configurations/trex_configs/intel_40g.yaml diff --git a/dts/nodes.example.yaml b/dts/configurations/nodes.example.yaml similarity index 100% rename from dts/nodes.example.yaml rename to dts/configurations/nodes.example.yaml diff --git a/dts/test_run.example.yaml b/dts/configurations/test_run.example.yaml similarity index 82% rename from dts/test_run.example.yaml rename to dts/configurations/test_run.example.yaml index 1bc436eed1..49088b44b6 100644 --- a/dts/test_run.example.yaml +++ b/dts/configurations/test_run.example.yaml @@ -23,8 +23,14 @@ dpdk: # in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options` # to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be # defined, but not both. -traffic_generator: +func_traffic_generator: type: SCAPY + remote_path: "" # The remote path of the traffic generator application. (Leave blank for SCAPY) + config: "" # Additional configuration files. (Leave blank if not required) +perf_traffic_generator: + type: TREX + remote_path: "/opt/trex/v3.03" # The remote path of the traffic generator application. (Leave blank for SCAPY) + config: "trex_config.yaml" # Additional configuration files. (Leave blank if not required) perf: false # disable performance testing func: true # enable functional testing skip_smoke_tests: true # optional diff --git a/dts/tests_config.example.yaml b/dts/configurations/tests_config.example.yaml similarity index 100% rename from dts/tests_config.example.yaml rename to dts/configurations/tests_config.example.yaml diff --git a/dts/configurations/trex_configs/intel_40g.yaml b/dts/configurations/trex_configs/intel_40g.yaml new file mode 100644 index 0000000000..dae003795b --- /dev/null +++ b/dts/configurations/trex_configs/intel_40g.yaml @@ -0,0 +1,18 @@ +### Config file generated by dpdk_setup_ports.py ### + +- version: 2 + interfaces: ['11:00.0', '11:00.1'] + port_bandwidth_gb: 40 + port_info: + - dest_mac: 3c:fd:fe:d5:e5:f9 # MAC OF LOOPBACK TO IT'S DUAL INTERFACE + src_mac: 3c:fd:fe:d5:e5:f8 + - dest_mac: 3c:fd:fe:d5:e5:f8 # MAC OF LOOPBACK TO IT'S DUAL INTERFACE + src_mac: 3c:fd:fe:d5:e5:f9 + + platform: + master_thread_id: 0 + latency_thread_id: 7 + dual_if: + - socket: 0 + threads: [1,2,3,4,5,6] + diff --git a/dts/framework/settings.py b/dts/framework/settings.py index 3f21615223..ccf4df25b0 100644 --- a/dts/framework/settings.py +++ b/dts/framework/settings.py @@ -130,9 +130,11 @@ class Settings: """ #: - test_run_config_path: Path = Path(__file__).parent.parent.joinpath("test_run.yaml") + test_run_config_path: Path = Path(__file__).parent.parent.joinpath( + "configurations/test_run.yaml" + ) #: - nodes_config_path: Path = Path(__file__).parent.parent.joinpath("nodes.yaml") + nodes_config_path: Path = Path(__file__).parent.parent.joinpath("configurations/nodes.yaml") #: tests_config_path: Path | None = None #: -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC v2 1/6] dts: rework config module to support perf TGs 2025-05-16 20:18 ` [RFC v2 1/6] dts: rework config module to support perf TGs Nicholas Pratte @ 2025-05-20 20:33 ` Dean Marx 0 siblings, 0 replies; 35+ messages in thread From: Dean Marx @ 2025-05-20 20:33 UTC (permalink / raw) To: dev Cc: stephen, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb <snip> > + - dest_mac: 3c:fd:fe:d5:e5:f9 # MAC OF LOOPBACK TO IT'S DUAL INTERFACE > + src_mac: 3c:fd:fe:d5:e5:f8 > + - dest_mac: 3c:fd:fe:d5:e5:f8 # MAC OF LOOPBACK TO IT'S DUAL INTERFACE > + src_mac: 3c:fd:fe:d5:e5:f9 Should be its not it's I like the concept of having a configurations directory within DTS, those files were starting to add up anyways. Reviewed-by: Dean Marx <dmarx@iol.unh.edu> ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC v2 2/6] dts: rework traffic generator inheritance structure. 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte 2025-05-16 20:18 ` [RFC v2 1/6] dts: rework config module to support perf TGs Nicholas Pratte @ 2025-05-16 20:18 ` Nicholas Pratte 2025-05-21 20:36 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 3/6] dts: add asynchronous support to ssh sessions Nicholas Pratte ` (3 subsequent siblings) 5 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 20:18 UTC (permalink / raw) To: stephen, dmarx, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Cc: dev, Nicholas Pratte Rework TG class hierarchy to include performance traffic generators, in addition to capturing traffic generators. As such, methods garnered to capturing traffic have been moved to the CapturingTrafficGenerator subclass. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- .../capturing_traffic_generator.py | 34 +++++++++ .../performance_traffic_generator.py | 69 +++++++++++++++++++ .../traffic_generator/traffic_generator.py | 43 ------------ 3 files changed, 103 insertions(+), 43 deletions(-) create mode 100644 dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py diff --git a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py index 61e5033f0b..124a1e5b86 100644 --- a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py +++ b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py @@ -65,6 +65,40 @@ def is_capturing(self) -> bool: """This traffic generator can capture traffic.""" return True + def send_packet(self, packet: Packet, port: Port) -> None: + """Send `packet` and block until it is fully sent. + + Send `packet` on `port`, then wait until `packet` is fully sent. + + Args: + packet: The packet to send. + port: The egress port on the TG node. + """ + self.send_packets([packet], port) + + def send_packets(self, packets: list[Packet], port: Port) -> None: + """Send `packets` and block until they are fully sent. + + Send `packets` on `port`, then wait until `packets` are fully sent. + + Args: + packets: The packets to send. + port: The egress port on the TG node. + """ + self._logger.info(f"Sending packet{'s' if len(packets) > 1 else ''}.") + self._logger.debug(get_packet_summaries(packets)) + self._send_packets(packets, port) + + @abstractmethod + def _send_packets(self, packets: list[Packet], port: Port) -> None: + """The implementation of :method:`send_packets`. + + The subclasses must implement this method which sends `packets` on `port`. + The method should block until all `packets` are fully sent. + + What fully sent means is defined by the traffic generator. + """ + def send_packets_and_capture( self, packets: list[Packet], diff --git a/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py new file mode 100644 index 0000000000..54458325e2 --- /dev/null +++ b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py @@ -0,0 +1,69 @@ +"""Performance testing capable traffic generatiors.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Callable + +from scapy.packet import Packet + +from .traffic_generator import TrafficGenerator + + +@dataclass(slots=True) +class PerformanceTrafficStats(ABC): + """Data structure for stats offered by a given traffic generator.""" + + frame_size: int + tx_expected_bps: float + tx_recorded_bps: float + + +class PerformanceTrafficGenerator(TrafficGenerator): + """An Abstract Base Class for all performance-oriented traffic generators. + + Provides an intermediary interface for performance-based traffic generator. + """ + + _test_stats: list[PerformanceTrafficStats] + + @property + def is_capturing(self) -> bool: + """Used for synchronization.""" + return False + + @property + def last_results(self) -> PerformanceTrafficStats | None: + """Get the latest set of results from TG instance. + + Returns: + The most recent set of traffic statistics. + """ + return self._test_stats.pop(0) + + def generate_traffic_and_stats( + self, + packet: Packet, + duration: float, + ) -> PerformanceTrafficStats: + """Send packet traffic and acquire associated statistics.""" + return self._calculate_traffic_stats(packet, duration, self._generate_traffic) + + def setup(self, ports): + """Preliminary port setup prior to TG execution.""" + for port in self._tg_node.ports: + self._tg_node.main_session.configure_port_mtu(2000, port) + + def teardown(self, ports): + """Port teardown after TG execution.""" + for port in self._tg_node.ports: + self._tg_node.main_session.configure_port_mtu(1500, port) + + @abstractmethod + def _calculate_traffic_stats( + self, packet: Packet, duration: float, traffic_gen_callback: Callable[[Packet, float], str] + ) -> PerformanceTrafficStats: + """Calculate packet traffic stats based on TG output.""" + + @abstractmethod + def _generate_traffic(self, packet: Packet, duration: float) -> str: + """Implementation for :method:`generate_traffic_and_stats`.""" diff --git a/dts/framework/testbed_model/traffic_generator/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py index 6b9705d025..e6154ef1df 100644 --- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py +++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py @@ -11,13 +11,10 @@ from abc import ABC, abstractmethod from typing import Iterable -from scapy.packet import Packet - from framework.config.test_run import TrafficGeneratorConfig from framework.logger import DTSLogger, get_dts_logger from framework.testbed_model.node import Node from framework.testbed_model.port import Port -from framework.utils import get_packet_summaries class TrafficGenerator(ABC): @@ -54,46 +51,6 @@ def setup(self, ports: Iterable[Port], rx_port: Port): def teardown(self, ports: Iterable[Port]): """Teardown the traffic generator.""" - self.close() - - def send_packet(self, packet: Packet, port: Port) -> None: - """Send `packet` and block until it is fully sent. - - Send `packet` on `port`, then wait until `packet` is fully sent. - - Args: - packet: The packet to send. - port: The egress port on the TG node. - """ - self.send_packets([packet], port) - - def send_packets(self, packets: list[Packet], port: Port) -> None: - """Send `packets` and block until they are fully sent. - - Send `packets` on `port`, then wait until `packets` are fully sent. - - Args: - packets: The packets to send. - port: The egress port on the TG node. - """ - self._logger.info(f"Sending packet{'s' if len(packets) > 1 else ''}.") - self._logger.debug(get_packet_summaries(packets)) - self._send_packets(packets, port) - - @abstractmethod - def _send_packets(self, packets: list[Packet], port: Port) -> None: - """The implementation of :method:`send_packets`. - - The subclasses must implement this method which sends `packets` on `port`. - The method should block until all `packets` are fully sent. - - What fully sent means is defined by the traffic generator. - """ - - @property - def is_capturing(self) -> bool: - """This traffic generator can't capture traffic.""" - return False @abstractmethod def close(self) -> None: -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC v2 2/6] dts: rework traffic generator inheritance structure. 2025-05-16 20:18 ` [RFC v2 2/6] dts: rework traffic generator inheritance structure Nicholas Pratte @ 2025-05-21 20:36 ` Dean Marx 0 siblings, 0 replies; 35+ messages in thread From: Dean Marx @ 2025-05-21 20:36 UTC (permalink / raw) To: dev Cc: stephen, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb On Fri, May 16, 2025 at 4:19 PM Nicholas Pratte <npratte@iol.unh.edu> wrote: > > Rework TG class hierarchy to include performance traffic generators, in > addition to capturing traffic generators. As such, methods garnered > to capturing traffic have been moved to the CapturingTrafficGenerator > subclass. This commit message is worded a bit strangely to me, not sure garnered is the right word to use here. Also not sure what "as such" is referring to exactly <snip> > +"""Performance testing capable traffic generatiors.""" *generators Reviewed-by: Dean Marx <dmarx@iol.unh.edu> ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC v2 3/6] dts: add asynchronous support to ssh sessions. 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte 2025-05-16 20:18 ` [RFC v2 1/6] dts: rework config module to support perf TGs Nicholas Pratte 2025-05-16 20:18 ` [RFC v2 2/6] dts: rework traffic generator inheritance structure Nicholas Pratte @ 2025-05-16 20:18 ` Nicholas Pratte 2025-05-22 15:04 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 4/6] dts: add extended timeout option to interactive shells Nicholas Pratte ` (2 subsequent siblings) 5 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 20:18 UTC (permalink / raw) To: stephen, dmarx, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Cc: dev, Nicholas Pratte Execution of the TREX server process requires an SSH session rework to support asynchronous process management. Allowing access to asynchronous functionality allows developers to execute processes without hijacking the SSH session being used. In doing so, both timeout and runtime errors may be avoided. This functionality leverages Fabric's Promise class, which provides a join method to terminate the process when the process is done being used, providing more secure process control. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/framework/remote_session/ssh_session.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dts/framework/remote_session/ssh_session.py b/dts/framework/remote_session/ssh_session.py index e6e4704bc2..e7d8b9cf53 100644 --- a/dts/framework/remote_session/ssh_session.py +++ b/dts/framework/remote_session/ssh_session.py @@ -13,6 +13,7 @@ ThreadException, UnexpectedExit, ) +from invoke.runners import Promise from paramiko.ssh_exception import ( AuthenticationException, BadHostKeyException, @@ -99,6 +100,19 @@ def _send_command(self, command: str, timeout: float, env: dict | None) -> Comma return CommandResult(self.name, command, output.stdout, output.stderr, output.return_code) + def _send_async_command(self, command: str, timeout: float, env: dict | None) -> Promise: + try: + promise = self.session.run( + command, env=env, warn=True, hide=True, timeout=timeout, asynchronous=True + ) + return promise + except (UnexpectedExit, ThreadException) as e: + self._logger.exception(e) + raise SSHSessionDeadError(self.hostname) from e + except CommandTimedOut as e: + self._logger.exception(e) + raise SSHTimeoutError(command) from e + def is_alive(self) -> bool: """Overrides :meth:`~.remote_session.RemoteSession.is_alive`.""" return self.session.is_connected -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC v2 3/6] dts: add asynchronous support to ssh sessions. 2025-05-16 20:18 ` [RFC v2 3/6] dts: add asynchronous support to ssh sessions Nicholas Pratte @ 2025-05-22 15:04 ` Dean Marx 0 siblings, 0 replies; 35+ messages in thread From: Dean Marx @ 2025-05-22 15:04 UTC (permalink / raw) To: dev Cc: stephen, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Reviewed-by: Dean Marx <dmarx@iol.unh.edu> ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC v2 4/6] dts: add extended timeout option to interactive shells. 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte ` (2 preceding siblings ...) 2025-05-16 20:18 ` [RFC v2 3/6] dts: add asynchronous support to ssh sessions Nicholas Pratte @ 2025-05-16 20:18 ` Nicholas Pratte 2025-05-22 15:10 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 5/6] dts: add trex traffic generator to dts framework Nicholas Pratte 2025-05-16 20:18 ` [RFC v2 6/6] dts: add performance test functions to test suite api Nicholas Pratte 5 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 20:18 UTC (permalink / raw) To: stephen, dmarx, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Cc: dev, Nicholas Pratte stdin commands may require an explicit duration of time to facilitate a total execution. Add an extra parameter to interactive shells to handle these circumstances. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/framework/remote_session/interactive_shell.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py index ba8489eafa..5331b8c7d1 100644 --- a/dts/framework/remote_session/interactive_shell.py +++ b/dts/framework/remote_session/interactive_shell.py @@ -177,7 +177,11 @@ def start_application(self, prompt: str | None = None) -> None: get_ctx().shell_pool.register_shell(self) def send_command( - self, command: str, prompt: str | None = None, skip_first_line: bool = False + self, + command: str, + prompt: str | None = None, + skip_first_line: bool = False, + added_timeout: int = 0, ) -> str: """Send `command` and get all output before the expected ending string. @@ -195,6 +199,7 @@ def send_command( prompt: After sending the command, `send_command` will be expecting this string. If :data:`None`, will use the class's default prompt. skip_first_line: Skip the first line when capturing the output. + added_timeout: additional duration for a given command, if needed. Returns: All output in the buffer before expected string. @@ -213,6 +218,7 @@ def send_command( self._logger.info(f"Sending: '{command}'") if prompt is None: prompt = self._default_prompt + self._ssh_channel.settimeout(self._timeout + added_timeout) out: str = "" try: self._stdin.write(f"{command}{self._command_extra_chars}\n") @@ -236,6 +242,7 @@ def send_command( self._node.main_session.interactive_session.hostname ) from e finally: + self._ssh_channel.settimeout(self._timeout) self._logger.debug(f"Got output: {out}") return out -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC v2 4/6] dts: add extended timeout option to interactive shells. 2025-05-16 20:18 ` [RFC v2 4/6] dts: add extended timeout option to interactive shells Nicholas Pratte @ 2025-05-22 15:10 ` Dean Marx 0 siblings, 0 replies; 35+ messages in thread From: Dean Marx @ 2025-05-22 15:10 UTC (permalink / raw) To: dev Cc: stephen, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Reviewed-by: Dean Marx <dmarx@iol.unh.edu> ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC v2 5/6] dts: add trex traffic generator to dts framework 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte ` (3 preceding siblings ...) 2025-05-16 20:18 ` [RFC v2 4/6] dts: add extended timeout option to interactive shells Nicholas Pratte @ 2025-05-16 20:18 ` Nicholas Pratte 2025-05-22 16:55 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 6/6] dts: add performance test functions to test suite api Nicholas Pratte 5 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 20:18 UTC (permalink / raw) To: stephen, dmarx, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Cc: dev, Nicholas Pratte Implement the TREX traffic generator for use in the DTS framework. The provided implementation leverages TREX's stateless API automation library, via use of a Python shell. As such, limitation to specific TREX versions may be be needed. The DTS context has been modified to include a performance traffic generator in addition to a functional traffic generator. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/framework/config/test_run.py | 20 +- dts/framework/context.py | 11 +- dts/framework/test_run.py | 27 +- dts/framework/test_suite.py | 6 +- .../traffic_generator/__init__.py | 22 +- .../testbed_model/traffic_generator/trex.py | 292 ++++++++++++++++++ 6 files changed, 359 insertions(+), 19 deletions(-) create mode 100644 dts/framework/testbed_model/traffic_generator/trex.py diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py index b6e4099eeb..3e09005338 100644 --- a/dts/framework/config/test_run.py +++ b/dts/framework/config/test_run.py @@ -396,6 +396,8 @@ class TrafficGeneratorType(str, Enum): #: SCAPY = "SCAPY" + #: + TREX = "TREX" class TrafficGeneratorConfig(FrozenModel): @@ -404,6 +406,8 @@ class TrafficGeneratorConfig(FrozenModel): #: The traffic generator type the child class is required to define to be distinguished among #: others. type: TrafficGeneratorType + remote_path: PurePath + config: PurePath class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): @@ -412,8 +416,16 @@ class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): type: Literal[TrafficGeneratorType.SCAPY] +class TrexTrafficGeneratorConfig(TrafficGeneratorConfig): + """TREX traffic generator specific configuration.""" + + type: Literal[TrafficGeneratorType.TREX] + + #: A union type discriminating traffic generators by the `type` field. -TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")] +TrafficGeneratorConfigTypes = Annotated[ + TrexTrafficGeneratorConfig, ScapyTrafficGeneratorConfig, Field(discriminator="type") +] #: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores. LogicalCores = Annotated[ @@ -461,8 +473,10 @@ class TestRunConfiguration(FrozenModel): #: The DPDK configuration used to test. dpdk: DPDKConfiguration - #: The traffic generator configuration used to test. - traffic_generator: TrafficGeneratorConfigTypes + #: The traffic generator configuration used for functional tests. + func_traffic_generator: TrafficGeneratorConfig + #: The traffic generator configuration used for performance tests. + perf_traffic_generator: TrafficGeneratorConfig #: Whether to run performance tests. perf: bool #: Whether to run functional tests. diff --git a/dts/framework/context.py b/dts/framework/context.py index 4360bc8699..fb7ded2a10 100644 --- a/dts/framework/context.py +++ b/dts/framework/context.py @@ -16,7 +16,12 @@ if TYPE_CHECKING: from framework.remote_session.dpdk import DPDKBuildEnvironment, DPDKRuntimeEnvironment - from framework.testbed_model.traffic_generator.traffic_generator import TrafficGenerator + from framework.testbed_model.traffic_generator.capturing_traffic_generator import ( + CapturingTrafficGenerator, + ) + from framework.testbed_model.traffic_generator.performance_traffic_generator import ( + PerformanceTrafficGenerator, + ) P = ParamSpec("P") @@ -69,7 +74,9 @@ class Context: topology: Topology dpdk_build: "DPDKBuildEnvironment" dpdk: "DPDKRuntimeEnvironment" - tg: "TrafficGenerator" + tg_dpdk: "DPDKRuntimeEnvironment" + func_tg: "CapturingTrafficGenerator" + perf_tg: "PerformanceTrafficGenerator" local: LocalContext = field(default_factory=LocalContext) shell_pool: ShellPool = field(default_factory=ShellPool) diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py index 0fdc57ea9c..be1476081a 100644 --- a/dts/framework/test_run.py +++ b/dts/framework/test_run.py @@ -107,7 +107,7 @@ from types import MethodType from typing import ClassVar, Protocol, Union -from framework.config.test_run import TestRunConfiguration +from framework.config.test_run import TestRunConfiguration, TrafficGeneratorType from framework.context import Context, init_ctx from framework.exception import ( InternalError, @@ -204,10 +204,22 @@ def __init__( dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node) dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env) - traffic_generator = create_traffic_generator(config.traffic_generator, tg_node) + # There is definitely a better way to do this. + tg_dpdk_runtime_env = None + if config.perf: + tg_dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, tg_node, None) + func_traffic_generator = create_traffic_generator(config.func_traffic_generator, tg_node) + perf_traffic_generator = create_traffic_generator(config.perf_traffic_generator, tg_node) self.ctx = Context( - sut_node, tg_node, topology, dpdk_build_env, dpdk_runtime_env, traffic_generator + sut_node, + tg_node, + topology, + dpdk_build_env, + dpdk_runtime_env, + tg_dpdk_runtime_env, + func_traffic_generator, + perf_traffic_generator, ) self.result = result self.selected_tests = list(self.config.filter_tests(tests_config)) @@ -345,7 +357,9 @@ def next(self) -> State | None: test_run.ctx.sut_node.setup() test_run.ctx.tg_node.setup() test_run.ctx.dpdk.setup(test_run.ctx.topology.sut_ports) - test_run.ctx.tg.setup(test_run.ctx.topology.tg_ports, test_run.ctx.topology.tg_port_ingress) + test_run.ctx.tg_dpdk.setup(test_run.ctx.topology.tg_ports) + test_run.ctx.func_tg.setup(test_run.ctx.topology.tg_ports) + test_run.ctx.perf_tg.setup(test_run.ctx.topology.tg_ports) self.result.ports = test_run.ctx.topology.sut_ports + test_run.ctx.topology.tg_ports self.result.sut_info = test_run.ctx.sut_node.node_info @@ -430,8 +444,9 @@ def description(self) -> str: def next(self) -> State | None: """Next state.""" - self.test_run.ctx.shell_pool.terminate_current_pool() - self.test_run.ctx.tg.teardown(self.test_run.ctx.topology.tg_ports) + self.test_run.ctx.tg_dpdk.teardown(self.test_run.ctx.topology.tg_ports) + self.test_run.ctx.func_tg.teardown(self.test_run.ctx.topology.tg_ports) + self.test_run.ctx.perf_tg.teardown(self.test_run.ctx.topology.tg_ports) self.test_run.ctx.dpdk.teardown(self.test_run.ctx.topology.sut_ports) self.test_run.ctx.tg_node.teardown() self.test_run.ctx.sut_node.teardown() diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py index e07c327b77..507df508cb 100644 --- a/dts/framework/test_suite.py +++ b/dts/framework/test_suite.py @@ -254,11 +254,11 @@ def send_packets_and_capture( A list of received packets. """ assert isinstance( - self._ctx.tg, CapturingTrafficGenerator + self._ctx.func_tg, CapturingTrafficGenerator ), "Cannot capture with a non-capturing traffic generator" # TODO: implement @requires for types of traffic generator packets = self._adjust_addresses(packets) - return self._ctx.tg.send_packets_and_capture( + return self._ctx.func_tg.send_packets_and_capture( packets, self._ctx.topology.tg_port_egress, self._ctx.topology.tg_port_ingress, @@ -276,7 +276,7 @@ def send_packets( packets: Packets to send. """ packets = self._adjust_addresses(packets) - self._ctx.tg.send_packets(packets, self._ctx.topology.tg_port_egress) + self._ctx.func_tg.send_packets(packets, self._ctx.topology.tg_port_egress) def get_expected_packets( self, diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py index 2a259a6e6c..53125995cd 100644 --- a/dts/framework/testbed_model/traffic_generator/__init__.py +++ b/dts/framework/testbed_model/traffic_generator/__init__.py @@ -14,17 +14,27 @@ and a capturing traffic generator is required. """ -from framework.config.test_run import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig +from framework.config.test_run import ( + ScapyTrafficGeneratorConfig as ScapyTrafficGeneratorConfig, +) +from framework.config.test_run import ( + TrafficGeneratorConfig, + TrafficGeneratorType, +) +from framework.config.test_run import ( + TrexTrafficGeneratorConfig as TrexTrafficGeneratorConfig, +) from framework.exception import ConfigurationError from framework.testbed_model.node import Node -from .capturing_traffic_generator import CapturingTrafficGenerator from .scapy import ScapyTrafficGenerator +from .traffic_generator import TrafficGenerator +from .trex import TrexTrafficGenerator def create_traffic_generator( traffic_generator_config: TrafficGeneratorConfig, node: Node -) -> CapturingTrafficGenerator: +) -> TrafficGenerator: """The factory function for creating traffic generator objects from the test run configuration. Args: @@ -37,8 +47,10 @@ def create_traffic_generator( Raises: ConfigurationError: If an unknown traffic generator has been setup. """ - match traffic_generator_config: - case ScapyTrafficGeneratorConfig(): + match traffic_generator_config.type: + case TrafficGeneratorType.SCAPY: return ScapyTrafficGenerator(node, traffic_generator_config, privileged=True) + case TrafficGeneratorType.TREX: + return TrexTrafficGenerator(node, traffic_generator_config, privileged=True) case _: raise ConfigurationError(f"Unknown traffic generator: {traffic_generator_config.type}") diff --git a/dts/framework/testbed_model/traffic_generator/trex.py b/dts/framework/testbed_model/traffic_generator/trex.py new file mode 100644 index 0000000000..b517333ab0 --- /dev/null +++ b/dts/framework/testbed_model/traffic_generator/trex.py @@ -0,0 +1,292 @@ +"""Implementation for TREX performance traffic generator.""" + +import time +from dataclasses import dataclass +from enum import Flag, auto +from typing import Callable, ClassVar + +from invoke.runners import Promise +from scapy.packet import Packet + +from framework.config.node import NodeConfiguration +from framework.config.test_run import TrafficGeneratorConfig +from framework.exception import SSHTimeoutError +from framework.remote_session.python_shell import PythonShell +from framework.remote_session.ssh_session import SSHSession +from framework.testbed_model.linux_session import LinuxSession +from framework.testbed_model.node import Node, create_session +from framework.testbed_model.traffic_generator.performance_traffic_generator import ( + PerformanceTrafficGenerator, + PerformanceTrafficStats, +) + + +@dataclass +class TrexPerPortStats: + """Performance statistics on a per port basis. + + Attributes: + opackets: Number of packets sent. + obytes: Number of egress bytes sent. + tx_bps: Maximum bits per second transmitted. + tx_pps: Number of transmitted packets sent. + """ + + tx_bps: float + tx_pps: float + + +@dataclass +class TrexPerformanceStats(PerformanceTrafficStats): + """Data structure to store performance statistics for a given test run. + + Attributes: + packet: The packet that was sent in the test run. + frame_size: The total length of the frame. (L2 downward) + tx_expected_bps: The expected bits per second on a given NIC. + tx_expected_cps: ... + tx_expected_pps: The expected packets per second of a given NIC. + tx_pps: The recorded maximum packets per second of the tested NIC. + tx_cps: The recorded maximum cps of the tested NIC + tx_bps: The recorded maximum bits per second of the tested NIC. + obytes: Total bytes output during test run. + port_stats: A list of :class:`TrexPerPortStats` provided by TREX. + """ + + packet: Packet + frame_size: int + + tx_expected_bps: float + tx_expected_cps: float + tx_expected_pps: float + + tx_pps: float + tx_cps: float + tx_bps: float + + port_stats: list[TrexPerPortStats] | None + + +class TrexStatelessTXModes(Flag): + """Flags indicating TREX instance's current trasmission mode.""" + + CONTINUOUS = auto() + SINGLE_BURST = auto() + MULTI_BURST = auto() + + +class TrexTrafficGenerator(PythonShell, PerformanceTrafficGenerator): + """TREX traffic generator. + + This implementation leverages the stateless API library provided in the TREX installation. + + Attributes: + stl_client_name: The name of the stateless client used in the stateless API. + packet_stream_name: The name of the stateless packet stream used in the stateless API. + timeout_duration: Internal timeout for connection to the TREX server. + """ + + _os_session: LinuxSession + _server_remote_session: SSHSession + _trex_server_process: Promise + + _tg_config: TrafficGeneratorConfig + _node_config: NodeConfiguration + + _python_indentation: ClassVar[str] = " " * 4 + + stl_client_name: ClassVar[str] = "client" + packet_stream_name: ClassVar[str] = "stream" + + _streaming_mode: TrexStatelessTXModes = TrexStatelessTXModes.CONTINUOUS + + timeout_duration: int + + def __init__( + self, tg_node: Node, config: TrafficGeneratorConfig, timeout_duration: int = 5, **kwargs + ) -> None: + """Initialize the TREX server. + + Initializes needed OS sessions for the creation of the TREX server process. + + Attributes: + tg_node: TG node the TREX instance is operating on. + config: Traffic generator config provided for TREX instance. + timeout_duration: Internal timeout for connection to the TREX server. + """ + super().__init__(node=tg_node, config=config, tg_node=tg_node, **kwargs) + self._node_config = tg_node.config + self._tg_config = config + self.timeout_duration = timeout_duration + + # Create TREX server session. + self._tg_node._other_sessions.append( + create_session(self._tg_node.config, "TREX Server.", self._logger) + ) + self._os_session = self._tg_node._other_sessions[0] + self._server_remote_session = self._os_session.remote_session + + def setup(self, ports): + """Initialize and start a TREX server process. + + Binds TG ports to vfio-pci and starts the trex process. + + Attributes: + ports: Related ports utilized in TG instance. + """ + super().setup(ports) + # Start TREX server process. + try: + self._logger.info("Starting TREX server process: sending 45 second sleep.") + server_command = [ + f"cd {self._tg_config.remote_path}; {self._tg_config.remote_path}/t-rex-64", + f"--cfg {self._tg_config.config} -i" + ] + privileged_command = self._os_session._get_privileged_command(" ".join(server_command)) + self._trex_server_process = self._server_remote_session._send_async_command( + privileged_command, timeout=None, env=None + ) + self._logger.info(f"Sending: '{privileged_command}") + time.sleep(45) + except SSHTimeoutError as e: + self._logger.exception("Failed to start TREX server process.", e) + + # Start Python shell. + self.start_application() + self.send_command("import os") + # Parent directory: /opt/v3.03/automation/trex_control_plane/interactive + self.send_command( + f"os.chdir('{self._tg_config.remote_path}/automation/trex_control_plane/interactive')" + ) + + # Import stateless API components. + imports = [ + "import trex", + "import trex.stl", + "import trex.stl.trex_stl_client", + "import trex.stl.trex_stl_streams", + "import trex.stl.trex_stl_packet_builder_scapy", + "from scapy.layers.l2 import Ether", + "from scapy.layers.inet import IP", + "from scapy.packet import Raw", + ] + self.send_command("\n".join(imports)) + + stateless_client = [ + f"{self.stl_client_name} = trex.stl.trex_stl_client.STLClient(", + f"username='{self._node_config.user}',", + "server='127.0.0.1',", + f"sync_timeout={self.timeout_duration}", + ")", + ] + self.send_command(f"\n{self._python_indentation}".join(stateless_client)) + self.send_command(f"{self.stl_client_name}.connect()") + + def teardown(self, ports) -> None: + """Teardown the TREX server and stateless implementation. + + close the TREX server process, and stop the Python shell. + + Attributes: + ports: Associated ports used by the TREX instance. + """ + super().teardown(ports) + self.send_command(f"{self.stl_client_name}.disconnect()") + self._os_session.send_command(f"pkill 't-rex-64'", privileged=True) + self._trex_server_process.join() + self.close() + + def _calculate_traffic_stats( + self, packet: Packet, duration: float, callback: Callable[[Packet, float], str] + ) -> PerformanceTrafficStats: + """Calculate the traffic statistics, using provided TG output. + + Takes in the statistics output provided by the stateless API implementation, and collects + them into a performance statistics data structure. + + Attributes: + packet: The packet being used for the performance test. + duration: The duration of the test. + callback: The callback function used to generate the traffic. + """ + # Convert to a dictionary. + stats_output = eval(callback(packet, duration)) + global_output = stats_output.get("global", "ERROR - DATA NOT FOUND") + + print(type(stats_output)) + print(stats_output) + + print(type(global_output)) + print(global_output) + return TrexPerformanceStats( + len(packet), + packet, + stats_output.get("tx_expected_bps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_expected_cps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_expected_pps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_pps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_cps", "ERROR - DATA NOT FOUND"), + stats_output.get("tx_bps", "ERROR - DATA NOT FOUND"), + stats_output.get("obytes", "ERROR - DATA NOT FOUND"), + None, + ) + + def set_streaming_mode(self, streaming_mode: TrexStatelessTXModes) -> None: + """Set the streaming mode of the TREX instance.""" + # Streaming modes are mutually exclusive. + self._streaming_mode = self._streaming_mode & streaming_mode + + def _generate_traffic(self, packet: Packet, duration: float) -> str: + """Generate traffic using provided packet. + + Uses the provided packet to generate traffic for the provided duration. + + Attributes: + packet: The packet being used for the performance test. + duration: The duration of the test being performed. + + Returns: + a string output of statistics provided by the traffic generator. + """ + """Implementation for :method:`generate_traffic_and_stats`.""" + streaming_mode = "" + if self._streaming_mode == TrexStatelessTXModes.CONTINUOUS: + streaming_mode = "STLTXCont" + elif self._streaming_mode == TrexStatelessTXModes.SINGLE_BURST: + streaming_mode = "STLTXSingleBurst" + elif self._streaming_mode == TrexStatelessTXModes.MULTI_BURST: + streaming_mode = "STLTXMultiBurst" + + packet_stream = [ + f"{self.packet_stream_name} = trex.stl.trex_stl_streams.STLStream(", + f"name='Test_{len(packet)}_bytes',", + f"packet=trex.stl.trex_stl_packet_builder_scapy.STLPktBuilder(pkt={packet.command()}),", + f"mode=trex.stl.trex_stl_streams.{streaming_mode}(),", + ")", + ] + self.send_command("\n".join(packet_stream)) + + # Prepare TREX console for next performance test. + procedure = [ + f"{self.stl_client_name}.connect()", + f"{self.stl_client_name}.reset(ports = [0, 1])", + f"{self.stl_client_name}.add_streams({self.packet_stream_name}, ports=[0, 1])", + f"{self.stl_client_name}.clear_stats()", + ")", + ] + self.send_command("\n".join(procedure)) + + start_test = [ + f"{self.stl_client_name}.start(ports=[0, 1], duration={duration})", + f"{self.stl_client_name}.wait_on_traffic(ports=[0, 1])", + ] + self._timeout = duration + self.send_command("\n".join(start_test), added_timeout=duration) + import time + + time.sleep(duration + 1) + + # Gather statistics output for parsing. + return self.send_command( + f"{self.stl_client_name}.get_stats(ports=[0, 1])", skip_first_line=True + ) -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC v2 5/6] dts: add trex traffic generator to dts framework 2025-05-16 20:18 ` [RFC v2 5/6] dts: add trex traffic generator to dts framework Nicholas Pratte @ 2025-05-22 16:55 ` Dean Marx 0 siblings, 0 replies; 35+ messages in thread From: Dean Marx @ 2025-05-22 16:55 UTC (permalink / raw) To: dev Cc: stephen, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Reviewed-by: Dean Marx <dmarx@iol.unh.edu> ^ permalink raw reply [flat|nested] 35+ messages in thread
* [RFC v2 6/6] dts: add performance test functions to test suite api 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte ` (4 preceding siblings ...) 2025-05-16 20:18 ` [RFC v2 5/6] dts: add trex traffic generator to dts framework Nicholas Pratte @ 2025-05-16 20:18 ` Nicholas Pratte 2025-05-22 17:54 ` Dean Marx 5 siblings, 1 reply; 35+ messages in thread From: Nicholas Pratte @ 2025-05-16 20:18 UTC (permalink / raw) To: stephen, dmarx, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb Cc: dev, Nicholas Pratte Provide functional performance method to run performance tests using a user-supplied performance traffic generator. The single core performance test is included, with some basic statistics checks verifying TG packet transmission rates. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> --- dts/configurations/tests_config.example.yaml | 5 ++ dts/framework/test_suite.py | 27 ++++++++++ dts/tests/TestSuite_single_core_perf.py | 56 ++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 dts/tests/TestSuite_single_core_perf.py diff --git a/dts/configurations/tests_config.example.yaml b/dts/configurations/tests_config.example.yaml index 38cf7a0dce..d3d867ae18 100644 --- a/dts/configurations/tests_config.example.yaml +++ b/dts/configurations/tests_config.example.yaml @@ -2,3 +2,8 @@ hello_world: msg: A custom hello world to you! +single_core_perf: + tx_rx_descriptors: [128, 512, 2048] + frame_sizes: [64, 128] + expected_throughput: 40 + # rate: gbps | mbps \ No newline at end of file diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py index 507df508cb..a89faac2d5 100644 --- a/dts/framework/test_suite.py +++ b/dts/framework/test_suite.py @@ -38,6 +38,10 @@ CapturingTrafficGenerator, PacketFilteringConfig, ) +from framework.testbed_model.traffic_generator.performance_traffic_generator import ( + PerformanceTrafficGenerator, + PerformanceTrafficStats, +) from .exception import ConfigurationError, InternalError, TestCaseVerifyError from .logger import DTSLogger, get_dts_logger @@ -266,6 +270,26 @@ def send_packets_and_capture( duration, ) + def assess_performance_by_packet( + self, packet: Packet, duration: int = 60 + ) -> PerformanceTrafficStats: + """Send a given packet for a given duration and assess basic performance statistics. + + Send `packet` and assess NIC performance for a given duration, corresponding to the test + suite's given topology. + + Args: + packet: The packet to send. + duration: Performance test duration (in seconds) + + Returns: + Performance statistics of the generated test. + """ + assert isinstance( + self._ctx.perf_tg, PerformanceTrafficGenerator + ), "Cannot run performance tests on non-performance traffic generator." + return self._ctx.perf_tg.generate_traffic_and_stats(packet, duration) + def send_packets( self, packets: list[Packet], @@ -275,6 +299,9 @@ def send_packets( Args: packets: Packets to send. """ + assert isinstance( + self._ctx.perf_tg, CapturingTrafficGenerator + ), "Cannot run performance tests on non-capturing traffic generator." packets = self._adjust_addresses(packets) self._ctx.func_tg.send_packets(packets, self._ctx.topology.tg_port_egress) diff --git a/dts/tests/TestSuite_single_core_perf.py b/dts/tests/TestSuite_single_core_perf.py new file mode 100644 index 0000000000..2e1d3c1ae8 --- /dev/null +++ b/dts/tests/TestSuite_single_core_perf.py @@ -0,0 +1,56 @@ +"""Single core performance test suite.""" + + +from framework.params.testpmd import RXRingParams, TXRingParams +from framework.remote_session.testpmd_shell import TestPmdShell +from framework.test_suite import TestSuite, perf_test, BaseConfig + +from scapy.layers.inet import IP +from scapy.layers.l2 import Ether +from scapy.packet import Raw + +class Config(BaseConfig): + """Performance test metrics to be compared by real-world results.""" + + #: Expected results + tx_rx_descriptors: list[int] + frame_sizes: list[int] + expected_throughput: int + + +class TestSingleCorePerf(TestSuite): + """Single core performance test suite.""" + + config: Config + + frame_sizes: list[int] + tx_rx_descriptors: list[int] + expected_throughput: int + + + def set_up_suite(self): + self.frame_sizes = self.config.frame_sizes + for frame_size in self.frame_sizes: + self.verify(frame_size >= 34, + "Provided frame size is too small. (Space needed for Ether()/IP())" + ) + self.tx_rx_descriptors = self.config.tx_rx_descriptors + self.expected_throughput = self.config.expected_throughput + + @perf_test + def test_perf_nic_single_core(self) -> None: + """Prototype test case.""" + for frame_size in self.frame_sizes: + for descriptor_size in self.tx_rx_descriptors: + with TestPmdShell( + tx_ring=TXRingParams(descriptors=descriptor_size), + rx_ring=RXRingParams(descriptors=descriptor_size) + ) as testpmd: + packet = Ether() / IP() / Raw(load="x" * (frame_size - 14 - 20)) + + testpmd.start() + stats = self.assess_performance_by_packet(packet, duration=60) + print(stats) + self.verify( + stats.tx_expected_bps == 40, "Expected output does not match recorded output." + ) \ No newline at end of file -- 2.47.1 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [RFC v2 6/6] dts: add performance test functions to test suite api 2025-05-16 20:18 ` [RFC v2 6/6] dts: add performance test functions to test suite api Nicholas Pratte @ 2025-05-22 17:54 ` Dean Marx 0 siblings, 0 replies; 35+ messages in thread From: Dean Marx @ 2025-05-22 17:54 UTC (permalink / raw) To: Nicholas Pratte Cc: stephen, luca.vizzarro, paul.szczepanek, yoan.picchi, Honnappa.Nagarahalli, thomas, thomas.wilks, probb, dev Reviewed-by: Dean Marx <dmarx@iol.unh.edu> ^ permalink raw reply [flat|nested] 35+ messages in thread
* [PATCH v3 1/5] dts: rework config module to support perf TGs 2025-04-23 19:40 [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework Nicholas Pratte ` (5 preceding siblings ...) 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte @ 2025-07-02 5:21 ` Patrick Robb 2025-07-02 5:21 ` [PATCH v3 2/5] dts: rework traffic generator inheritance structure Patrick Robb ` (4 more replies) 6 siblings, 5 replies; 35+ messages in thread From: Patrick Robb @ 2025-07-02 5:21 UTC (permalink / raw) To: Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, Luca.Vizzarro, thomas.wilks, Nicholas Pratte, Patrick Robb From: Nicholas Pratte <npratte@iol.unh.edu> Rework test run configuration file for TGs to support both application directory location and any necessary configuration files; an example TREX configuration file is provided. Configuration files have been moved to a configurations directory, requiring a slight modification to the settings module. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> Signed-off-by: Patrick Robb <probb@iol.unh.edu> Reviewed-by: Dean Marx <dmarx@iol.unh.edu> --- dts/{ => configurations}/nodes.example.yaml | 0 dts/{ => configurations}/test_run.example.yaml | 13 +++++++------ dts/{ => configurations}/tests_config.example.yaml | 0 dts/framework/settings.py | 6 ++++-- 4 files changed, 11 insertions(+), 8 deletions(-) rename dts/{ => configurations}/nodes.example.yaml (100%) rename dts/{ => configurations}/test_run.example.yaml (80%) rename dts/{ => configurations}/tests_config.example.yaml (100%) diff --git a/dts/nodes.example.yaml b/dts/configurations/nodes.example.yaml similarity index 100% rename from dts/nodes.example.yaml rename to dts/configurations/nodes.example.yaml diff --git a/dts/test_run.example.yaml b/dts/configurations/test_run.example.yaml similarity index 80% rename from dts/test_run.example.yaml rename to dts/configurations/test_run.example.yaml index 1bc436eed1..e9baf10035 100644 --- a/dts/test_run.example.yaml +++ b/dts/configurations/test_run.example.yaml @@ -1,8 +1,3 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright 2022-2023 The DPDK contributors -# Copyright 2023 Arm Limited - -# Define the test run environment dpdk: lcores: "" # use all available logical cores (Skips first core) memory_channels: 4 # tells DPDK to use 4 memory channels @@ -23,8 +18,14 @@ dpdk: # in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options` # to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be # defined, but not both. -traffic_generator: +func_traffic_generator: type: SCAPY + remote_path: "" # The remote path of the traffic generator application. (Leave blank for SCAPY) + config: "" # Additional configuration files. (Leave blank if not required) +perf_traffic_generator: + type: TREX + remote_path: "/opt/trex/v3.03" # The remote path of the traffic generator application. (Leave blank for SCAPY) + config: "/opt/trex_config/trex_config.yaml" # Additional configuration files. (Leave blank if not required) perf: false # disable performance testing func: true # enable functional testing skip_smoke_tests: true # optional diff --git a/dts/tests_config.example.yaml b/dts/configurations/tests_config.example.yaml similarity index 100% rename from dts/tests_config.example.yaml rename to dts/configurations/tests_config.example.yaml diff --git a/dts/framework/settings.py b/dts/framework/settings.py index 3f21615223..ccf4df25b0 100644 --- a/dts/framework/settings.py +++ b/dts/framework/settings.py @@ -130,9 +130,11 @@ class Settings: """ #: - test_run_config_path: Path = Path(__file__).parent.parent.joinpath("test_run.yaml") + test_run_config_path: Path = Path(__file__).parent.parent.joinpath( + "configurations/test_run.yaml" + ) #: - nodes_config_path: Path = Path(__file__).parent.parent.joinpath("nodes.yaml") + nodes_config_path: Path = Path(__file__).parent.parent.joinpath("configurations/nodes.yaml") #: tests_config_path: Path | None = None #: -- 2.49.0 ^ permalink raw reply [flat|nested] 35+ messages in thread
* [PATCH v3 2/5] dts: rework traffic generator inheritance structure 2025-07-02 5:21 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Patrick Robb @ 2025-07-02 5:21 ` Patrick Robb 2025-07-02 15:31 ` Luca Vizzarro 2025-07-02 5:21 ` [PATCH v3 3/5] dts: add timeout override option to interactive shells Patrick Robb ` (3 subsequent siblings) 4 siblings, 1 reply; 35+ messages in thread From: Patrick Robb @ 2025-07-02 5:21 UTC (permalink / raw) To: Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, Luca.Vizzarro, thomas.wilks, Nicholas Pratte, Patrick Robb From: Nicholas Pratte <npratte@iol.unh.edu> Rework TG class hierarchy to include performance traffic generators. As such, methods specific to capturing traffic have been moved to the CapturingTrafficGenerator subclass. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> Signed-off-by: Patrick Robb <probb@iol.unh.edu> Reviewed-by: Dean Marx <dmarx@iol.unh.edu> --- .../capturing_traffic_generator.py | 34 ++++++++++ .../performance_traffic_generator.py | 63 +++++++++++++++++++ .../testbed_model/traffic_generator/scapy.py | 10 ++- .../traffic_generator/traffic_generator.py | 44 ------------- 4 files changed, 104 insertions(+), 47 deletions(-) create mode 100644 dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py diff --git a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py index a85858ba07..110244a7b0 100644 --- a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py +++ b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py @@ -65,6 +65,40 @@ def is_capturing(self) -> bool: """This traffic generator can capture traffic.""" return True + def send_packet(self, packet: Packet, port: Port) -> None: + """Send `packet` and block until it is fully sent. + + Send `packet` on `port`, then wait until `packet` is fully sent. + + Args: + packet: The packet to send. + port: The egress port on the TG node. + """ + self.send_packets([packet], port) + + def send_packets(self, packets: list[Packet], port: Port) -> None: + """Send `packets` and block until they are fully sent. + + Send `packets` on `port`, then wait until `packets` are fully sent. + + Args: + packets: The packets to send. + port: The egress port on the TG node. + """ + self._logger.info(f"Sending packet{'s' if len(packets) > 1 else ''}.") + self._logger.debug(get_packet_summaries(packets)) + self._send_packets(packets, port) + + @abstractmethod + def _send_packets(self, packets: list[Packet], port: Port) -> None: + """The implementation of :method:`send_packets`. + + The subclasses must implement this method which sends `packets` on `port`. + The method should block until all `packets` are fully sent. + + What fully sent means is defined by the traffic generator. + """ + def send_packets_and_capture( self, packets: list[Packet], diff --git a/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py new file mode 100644 index 0000000000..59dac6a075 --- /dev/null +++ b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py @@ -0,0 +1,63 @@ +"""Traffic generators for performance tests which can generate a high number of packets.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +from scapy.packet import Packet + +from framework.testbed_model.topology import Topology + +from .traffic_generator import TrafficGenerator + + +@dataclass(slots=True) +class PerformanceTrafficStats(ABC): + """Data structure to store performance statistics for a given test run. + + Attributes: + frame_size: The total length of the frame + tx_pps: Recorded tx packets per second + tx_cps: Recorded tx connections per second + tx_bps: Recorded tx bytes per second + rx_pps: Recorded rx packets per second + rx_bps: Recorded rx bytes per second + """ + + frame_size: int + + tx_pps: float + tx_cps: float + tx_bps: float + + rx_pps: float + rx_bps: float + + +class PerformanceTrafficGenerator(TrafficGenerator): + """An abstract base class for all performance-oriented traffic generators. + + Provides an intermediary interface for performance-based traffic generator. + """ + + @abstractmethod + def calculate_traffic_and_stats( + self, + packet: Packet, + send_mpps: int, + duration: float, + ) -> PerformanceTrafficStats: + """Send packet traffic and acquire associated statistics. + + Args: + packet: The packet to send. + send_mpps: The millions packets per second send rate. + duration: Performance test duration (in seconds). + + Returns: + Performance statistics of the generated test. + """ + + def setup(self, topology: Topology) -> None: + """Overrides :meth:`.traffic_generator.TrafficGenerator.setup`.""" + for port in self._tg_node.ports: + self._tg_node.main_session.configure_port_mtu(2000, port) diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py index e21ba4ed96..602b93d473 100644 --- a/dts/framework/testbed_model/traffic_generator/scapy.py +++ b/dts/framework/testbed_model/traffic_generator/scapy.py @@ -26,7 +26,7 @@ from scapy.packet import Packet from framework.config.node import OS -from framework.config.test_run import ScapyTrafficGeneratorConfig +from framework.config.test_run import TrafficGeneratorConfig from framework.exception import InteractiveSSHSessionDeadError, InternalError from framework.remote_session.python_shell import PythonShell from framework.testbed_model.node import Node @@ -285,7 +285,7 @@ class also extends :class:`.capturing_traffic_generator.CapturingTrafficGenerato first. """ - _config: ScapyTrafficGeneratorConfig + _config: TrafficGeneratorConfig _shell: PythonShell _sniffer: ScapyAsyncSniffer @@ -296,7 +296,7 @@ class also extends :class:`.capturing_traffic_generator.CapturingTrafficGenerato #: Padding to add to the start of a line for python syntax compliance. _python_indentation: ClassVar[str] = " " * 4 - def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig, **kwargs): + def __init__(self, tg_node: Node, config: TrafficGeneratorConfig, **kwargs): """Extend the constructor with Scapy TG specifics. Initializes both the traffic generator and the interactive shell used to handle Scapy @@ -332,6 +332,10 @@ def setup(self, topology: Topology): self._shell.send_command("from scapy.all import *") self._shell.send_command("from scapy.contrib.lldp import *") + def teardown(self): + """Overrides :meth:`.traffic_generator.TrafficGenerator.teardown`.""" + pass + def close(self): """Overrides :meth:`.traffic_generator.TrafficGenerator.close`. diff --git a/dts/framework/testbed_model/traffic_generator/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py index 8f53b07daf..ea3075989d 100644 --- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py +++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py @@ -10,14 +10,10 @@ from abc import ABC, abstractmethod -from scapy.packet import Packet - from framework.config.test_run import TrafficGeneratorConfig from framework.logger import DTSLogger, get_dts_logger from framework.testbed_model.node import Node -from framework.testbed_model.port import Port from framework.testbed_model.topology import Topology -from framework.utils import get_packet_summaries class TrafficGenerator(ABC): @@ -54,46 +50,6 @@ def setup(self, topology: Topology): def teardown(self): """Teardown the traffic generator.""" - self.close() - - def send_packet(self, packet: Packet, port: Port) -> None: - """Send `packet` and block until it is fully sent. - - Send `packet` on `port`, then wait until `packet` is fully sent. - - Args: - packet: The packet to send. - port: The egress port on the TG node. - """ - self.send_packets([packet], port) - - def send_packets(self, packets: list[Packet], port: Port) -> None: - """Send `packets` and block until they are fully sent. - - Send `packets` on `port`, then wait until `packets` are fully sent. - - Args: - packets: The packets to send. - port: The egress port on the TG node. - """ - self._logger.info(f"Sending packet{'s' if len(packets) > 1 else ''}.") - self._logger.debug(get_packet_summaries(packets)) - self._send_packets(packets, port) - - @abstractmethod - def _send_packets(self, packets: list[Packet], port: Port) -> None: - """The implementation of :method:`send_packets`. - - The subclasses must implement this method which sends `packets` on `port`. - The method should block until all `packets` are fully sent. - - What fully sent means is defined by the traffic generator. - """ - - @property - def is_capturing(self) -> bool: - """This traffic generator can't capture traffic.""" - return False @abstractmethod def close(self) -> None: -- 2.49.0 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [PATCH v3 2/5] dts: rework traffic generator inheritance structure 2025-07-02 5:21 ` [PATCH v3 2/5] dts: rework traffic generator inheritance structure Patrick Robb @ 2025-07-02 15:31 ` Luca Vizzarro 0 siblings, 0 replies; 35+ messages in thread From: Luca Vizzarro @ 2025-07-02 15:31 UTC (permalink / raw) To: Patrick Robb, Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, thomas.wilks, Nicholas Pratte On 02/07/2025 06:21, Patrick Robb wrote: > +++ b/dts/framework/testbed_model/traffic_generator/performance_traffic_generator.py > @@ -0,0 +1,63 @@ > <snip> > + > +@dataclass(slots=True) > +class PerformanceTrafficStats(ABC): Why is this inheriting ABC? I don't see why a dataclass would be abstract given it's even missing abstract methods. > <snip> > +class PerformanceTrafficGenerator(TrafficGenerator): > + """An abstract base class for all performance-oriented traffic generators. > + > + Provides an intermediary interface for performance-based traffic generator. > + """ > + > + @abstractmethod > + def calculate_traffic_and_stats( > + self, > + packet: Packet, > + send_mpps: int, > + duration: float, > + ) -> PerformanceTrafficStats: > + """Send packet traffic and acquire associated statistics. > + > + Args: > + packet: The packet to send. > + send_mpps: The millions packets per second send rate. > + duration: Performance test duration (in seconds). bad indentation for args. > diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py > index e21ba4ed96..602b93d473 100644 > --- a/dts/framework/testbed_model/traffic_generator/scapy.py > +++ b/dts/framework/testbed_model/traffic_generator/scapy.py > @@ -26,7 +26,7 @@ > from scapy.packet import Packet > > from framework.config.node import OS > -from framework.config.test_run import ScapyTrafficGeneratorConfig > +from framework.config.test_run import TrafficGeneratorConfig Making the Scapy class use a more generic config class looks like breaking behaviour. Why is this happening? > @@ -332,6 +332,10 @@ def setup(self, topology: Topology): > self._shell.send_command("from scapy.all import *") > self._shell.send_command("from scapy.contrib.lldp import *") > > + def teardown(self): > + """Overrides :meth:`.traffic_generator.TrafficGenerator.teardown`.""" > + pass nit: `pass` is not needed here, because the docstring already completes the method as noop. > <snip> > diff --git a/dts/framework/testbed_model/traffic_generator/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py > index 8f53b07daf..ea3075989d 100644 > --- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py > +++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py > @@ -10,14 +10,10 @@ > <snip> > > class TrafficGenerator(ABC): > @@ -54,46 +50,6 @@ def setup(self, topology: Topology): > > def teardown(self): > """Teardown the traffic generator.""" > - self.close() how come are we removing the default close behaviour? I don't see it being replaced appropriately. > <snip> > - @property > - def is_capturing(self) -> bool: > - """This traffic generator can't capture traffic.""" > - return False Why is the above property being removed? > > @abstractmethod > def close(self) -> None: ^ permalink raw reply [flat|nested] 35+ messages in thread
* [PATCH v3 3/5] dts: add timeout override option to interactive shells 2025-07-02 5:21 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Patrick Robb 2025-07-02 5:21 ` [PATCH v3 2/5] dts: rework traffic generator inheritance structure Patrick Robb @ 2025-07-02 5:21 ` Patrick Robb 2025-07-02 15:33 ` Luca Vizzarro 2025-07-02 5:21 ` [PATCH v3 4/5] dts: add trex traffic generator to dts framework Patrick Robb ` (2 subsequent siblings) 4 siblings, 1 reply; 35+ messages in thread From: Patrick Robb @ 2025-07-02 5:21 UTC (permalink / raw) To: Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, Luca.Vizzarro, thomas.wilks, Nicholas Pratte, Patrick Robb From: Nicholas Pratte <npratte@iol.unh.edu> Add an extra parameter to the interactive shell send command to handle function to allow for a 1 time override of the send command timeout. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> Signed-off-by: Patrick Robb <probb@iol.unh.edu> Reviewed-by: Dean Marx <dmarx@iol.unh.edu> --- dts/framework/remote_session/interactive_shell.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py index ba8489eafa..5331b8c7d1 100644 --- a/dts/framework/remote_session/interactive_shell.py +++ b/dts/framework/remote_session/interactive_shell.py @@ -177,7 +177,11 @@ def start_application(self, prompt: str | None = None) -> None: get_ctx().shell_pool.register_shell(self) def send_command( - self, command: str, prompt: str | None = None, skip_first_line: bool = False + self, + command: str, + prompt: str | None = None, + skip_first_line: bool = False, + added_timeout: int = 0, ) -> str: """Send `command` and get all output before the expected ending string. @@ -195,6 +199,7 @@ def send_command( prompt: After sending the command, `send_command` will be expecting this string. If :data:`None`, will use the class's default prompt. skip_first_line: Skip the first line when capturing the output. + added_timeout: additional duration for a given command, if needed. Returns: All output in the buffer before expected string. @@ -213,6 +218,7 @@ def send_command( self._logger.info(f"Sending: '{command}'") if prompt is None: prompt = self._default_prompt + self._ssh_channel.settimeout(self._timeout + added_timeout) out: str = "" try: self._stdin.write(f"{command}{self._command_extra_chars}\n") @@ -236,6 +242,7 @@ def send_command( self._node.main_session.interactive_session.hostname ) from e finally: + self._ssh_channel.settimeout(self._timeout) self._logger.debug(f"Got output: {out}") return out -- 2.49.0 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [PATCH v3 3/5] dts: add timeout override option to interactive shells 2025-07-02 5:21 ` [PATCH v3 3/5] dts: add timeout override option to interactive shells Patrick Robb @ 2025-07-02 15:33 ` Luca Vizzarro 0 siblings, 0 replies; 35+ messages in thread From: Luca Vizzarro @ 2025-07-02 15:33 UTC (permalink / raw) To: Patrick Robb, Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, thomas.wilks, Nicholas Pratte Reviewed-by: Luca Vizzarro <luca.vizzarro@arm.com> ^ permalink raw reply [flat|nested] 35+ messages in thread
* [PATCH v3 4/5] dts: add trex traffic generator to dts framework 2025-07-02 5:21 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Patrick Robb 2025-07-02 5:21 ` [PATCH v3 2/5] dts: rework traffic generator inheritance structure Patrick Robb 2025-07-02 5:21 ` [PATCH v3 3/5] dts: add timeout override option to interactive shells Patrick Robb @ 2025-07-02 5:21 ` Patrick Robb 2025-07-02 16:32 ` Luca Vizzarro 2025-07-02 5:21 ` [PATCH v3 5/5] dts: add performance test functions to test suite API Patrick Robb 2025-07-02 15:09 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Luca Vizzarro 4 siblings, 1 reply; 35+ messages in thread From: Patrick Robb @ 2025-07-02 5:21 UTC (permalink / raw) To: Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, Luca.Vizzarro, thomas.wilks, Nicholas Pratte, Patrick Robb From: Nicholas Pratte <npratte@iol.unh.edu> Implement the TREX traffic generator for use in the DTS framework. The provided implementation leverages TREX's stateless API automation library, via use of a Python shell. As such, limitation to specific TREX versions may be needed. The DTS context has been modified to include a performance traffic generator in addition to a functional traffic generator. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> Signed-off-by: Patrick Robb <probb@iol.unh.edu> Reviewed-by: Dean Marx <dmarx@iol.unh.edu> --- dts/framework/config/test_run.py | 20 +- dts/framework/context.py | 2 +- dts/framework/test_run.py | 12 +- dts/framework/test_suite.py | 6 +- .../traffic_generator/__init__.py | 22 +- .../testbed_model/traffic_generator/trex.py | 258 ++++++++++++++++++ 6 files changed, 306 insertions(+), 14 deletions(-) create mode 100644 dts/framework/testbed_model/traffic_generator/trex.py diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py index b6e4099eeb..3e09005338 100644 --- a/dts/framework/config/test_run.py +++ b/dts/framework/config/test_run.py @@ -396,6 +396,8 @@ class TrafficGeneratorType(str, Enum): #: SCAPY = "SCAPY" + #: + TREX = "TREX" class TrafficGeneratorConfig(FrozenModel): @@ -404,6 +406,8 @@ class TrafficGeneratorConfig(FrozenModel): #: The traffic generator type the child class is required to define to be distinguished among #: others. type: TrafficGeneratorType + remote_path: PurePath + config: PurePath class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): @@ -412,8 +416,16 @@ class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): type: Literal[TrafficGeneratorType.SCAPY] +class TrexTrafficGeneratorConfig(TrafficGeneratorConfig): + """TREX traffic generator specific configuration.""" + + type: Literal[TrafficGeneratorType.TREX] + + #: A union type discriminating traffic generators by the `type` field. -TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")] +TrafficGeneratorConfigTypes = Annotated[ + TrexTrafficGeneratorConfig, ScapyTrafficGeneratorConfig, Field(discriminator="type") +] #: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores. LogicalCores = Annotated[ @@ -461,8 +473,10 @@ class TestRunConfiguration(FrozenModel): #: The DPDK configuration used to test. dpdk: DPDKConfiguration - #: The traffic generator configuration used to test. - traffic_generator: TrafficGeneratorConfigTypes + #: The traffic generator configuration used for functional tests. + func_traffic_generator: TrafficGeneratorConfig + #: The traffic generator configuration used for performance tests. + perf_traffic_generator: TrafficGeneratorConfig #: Whether to run performance tests. perf: bool #: Whether to run functional tests. diff --git a/dts/framework/context.py b/dts/framework/context.py index 4360bc8699..8caac935a5 100644 --- a/dts/framework/context.py +++ b/dts/framework/context.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from framework.remote_session.dpdk import DPDKBuildEnvironment, DPDKRuntimeEnvironment - from framework.testbed_model.traffic_generator.traffic_generator import TrafficGenerator + from framework.testbed_model.traffic_generator import TrafficGenerator P = ParamSpec("P") diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py index 10a5e1a6b8..ea8ca41414 100644 --- a/dts/framework/test_run.py +++ b/dts/framework/test_run.py @@ -204,10 +204,18 @@ def __init__( dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node) dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env) - traffic_generator = create_traffic_generator(config.traffic_generator, tg_node) + if config.func: + traffic_generator = create_traffic_generator(config.func_traffic_generator, tg_node) + if config.perf: + traffic_generator = create_traffic_generator(config.perf_traffic_generator, tg_node) self.ctx = Context( - sut_node, tg_node, topology, dpdk_build_env, dpdk_runtime_env, traffic_generator + sut_node, + tg_node, + topology, + dpdk_build_env, + dpdk_runtime_env, + traffic_generator, ) self.result = result self.selected_tests = list(self.config.filter_tests(tests_config)) diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py index e5fbadd1a1..75d7a6eb6c 100644 --- a/dts/framework/test_suite.py +++ b/dts/framework/test_suite.py @@ -254,11 +254,11 @@ def send_packets_and_capture( A list of received packets. """ assert isinstance( - self._ctx.tg, CapturingTrafficGenerator + self._ctx.func_tg, CapturingTrafficGenerator ), "Cannot capture with a non-capturing traffic generator" # TODO: implement @requires for types of traffic generator packets = self._adjust_addresses(packets) - return self._ctx.tg.send_packets_and_capture( + return self._ctx.func_tg.send_packets_and_capture( packets, self._ctx.topology.tg_port_egress, self._ctx.topology.tg_port_ingress, @@ -276,7 +276,7 @@ def send_packets( packets: Packets to send. """ packets = self._adjust_addresses(packets) - self._ctx.tg.send_packets(packets, self._ctx.topology.tg_port_egress) + self._ctx.func_tg.send_packets(packets, self._ctx.topology.tg_port_egress) def get_expected_packets( self, diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py index 2a259a6e6c..53125995cd 100644 --- a/dts/framework/testbed_model/traffic_generator/__init__.py +++ b/dts/framework/testbed_model/traffic_generator/__init__.py @@ -14,17 +14,27 @@ and a capturing traffic generator is required. """ -from framework.config.test_run import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig +from framework.config.test_run import ( + ScapyTrafficGeneratorConfig as ScapyTrafficGeneratorConfig, +) +from framework.config.test_run import ( + TrafficGeneratorConfig, + TrafficGeneratorType, +) +from framework.config.test_run import ( + TrexTrafficGeneratorConfig as TrexTrafficGeneratorConfig, +) from framework.exception import ConfigurationError from framework.testbed_model.node import Node -from .capturing_traffic_generator import CapturingTrafficGenerator from .scapy import ScapyTrafficGenerator +from .traffic_generator import TrafficGenerator +from .trex import TrexTrafficGenerator def create_traffic_generator( traffic_generator_config: TrafficGeneratorConfig, node: Node -) -> CapturingTrafficGenerator: +) -> TrafficGenerator: """The factory function for creating traffic generator objects from the test run configuration. Args: @@ -37,8 +47,10 @@ def create_traffic_generator( Raises: ConfigurationError: If an unknown traffic generator has been setup. """ - match traffic_generator_config: - case ScapyTrafficGeneratorConfig(): + match traffic_generator_config.type: + case TrafficGeneratorType.SCAPY: return ScapyTrafficGenerator(node, traffic_generator_config, privileged=True) + case TrafficGeneratorType.TREX: + return TrexTrafficGenerator(node, traffic_generator_config, privileged=True) case _: raise ConfigurationError(f"Unknown traffic generator: {traffic_generator_config.type}") diff --git a/dts/framework/testbed_model/traffic_generator/trex.py b/dts/framework/testbed_model/traffic_generator/trex.py new file mode 100644 index 0000000000..1ba7d6bbbd --- /dev/null +++ b/dts/framework/testbed_model/traffic_generator/trex.py @@ -0,0 +1,258 @@ +"""Implementation for TREX performance traffic generator.""" + +import time +from enum import Flag, auto +from typing import Any, ClassVar + +from scapy.packet import Packet + +from framework.config.node import NodeConfiguration +from framework.config.test_run import TrafficGeneratorConfig +from framework.context import get_ctx +from framework.exception import SSHTimeoutError +from framework.remote_session.python_shell import PythonShell +from framework.testbed_model.node import Node, create_session +from framework.testbed_model.os_session import OSSession +from framework.testbed_model.topology import Topology +from framework.testbed_model.traffic_generator.performance_traffic_generator import ( + PerformanceTrafficGenerator, + PerformanceTrafficStats, +) + + +class TrexStatelessTXModes(Flag): + """Flags indicating TREX instance's current transmission mode.""" + + CONTINUOUS = auto() + SINGLE_BURST = auto() + MULTI_BURST = auto() + + +class TrexTrafficGenerator(PerformanceTrafficGenerator): + """TREX traffic generator. + + This implementation leverages the stateless API library provided in the TREX installation. + + Attributes: + stl_client_name: The name of the stateless client used in the stateless API. + packet_stream_name: The name of the stateless packet stream used in the stateless API. + """ + + _os_session: OSSession + _server_remote_session: Any + + _tg_config: TrafficGeneratorConfig + _node_config: NodeConfiguration + + _shell: PythonShell + _python_indentation: ClassVar[str] = " " * 4 + + stl_client_name: ClassVar[str] = "client" + packet_stream_name: ClassVar[str] = "stream" + + _streaming_mode: TrexStatelessTXModes = TrexStatelessTXModes.CONTINUOUS + + tg_cores: int = 10 + + def __init__(self, tg_node: Node, config: TrafficGeneratorConfig, **kwargs) -> None: + """Initialize the TREX server. + + Initializes needed OS sessions for the creation of the TREX server process. + + Attributes: + tg_node: TG node the TREX instance is operating on. + config: Traffic generator config provided for TREX instance. + """ + super().__init__(tg_node=tg_node, config=config, **kwargs) + self._tg_node_config = tg_node.config + self._tg_config = config + + self._os_session = create_session(self._tg_node.config, "TREX", self._logger) + self._server_remote_session = self._os_session.remote_session + + self._shell = PythonShell(self._tg_node, "TREX-client", privileged=True) + + def setup(self, topology: Topology): + """Initialize and start a TREX server process.""" + super().setup(get_ctx().topology) + # Start TREX server process. + try: + self._logger.info("Starting TREX server process: sending 20 second sleep.") + server_command = [ + f"cd {self._tg_config.remote_path};", + self._os_session._get_privileged_command( + f"screen -d -m ./t-rex-64 --cfg {self._tg_config.config} -c {self.tg_cores} -i" + ), + ] + privileged_command = " ".join(server_command) + self._logger.info(f"Sending: '{privileged_command}") + self._server_remote_session.session.run(privileged_command) + time.sleep(20) + + except SSHTimeoutError as e: + self._logger.exception("Failed to start TREX server process.", e) + + # Start Python shell. + self._shell.start_application() + self._shell.send_command("import os") + self._shell.send_command( + f"os.chdir('{self._tg_config.remote_path}/automation/trex_control_plane/interactive')" + ) + + # Import stateless API components. + imports = [ + "import trex", + "import trex.stl", + "import trex.stl.trex_stl_client", + "import trex.stl.trex_stl_streams", + "import trex.stl.trex_stl_packet_builder_scapy", + "from scapy.layers.l2 import Ether", + "from scapy.layers.inet import IP", + "from scapy.packet import Raw", + ] + self._shell.send_command("\n".join(imports)) + + stateless_client = [ + f"{self.stl_client_name} = trex.stl.trex_stl_client.STLClient(", + f"username='{self._tg_node_config.user}',", + "server='127.0.0.1',", + ")", + ] + + self._shell.send_command(f"\n{self._python_indentation}".join(stateless_client)) + self._shell.send_command(f"{self.stl_client_name}.connect()") + + def teardown(self) -> None: + """Teardown the TREX server and stateless implementation. + + close the TREX server process, and stop the Python shell. + + Attributes: + ports: Associated ports used by the TREX instance. + """ + super().teardown() + self._os_session.send_command("pkill t-rex-64", privileged=True) + self.close() + + def calculate_traffic_and_stats( + self, packet: Packet, send_mpps: int, duration: float + ) -> PerformanceTrafficStats: + """Calculate the traffic statistics, using provided TG output. + + Takes in the statistics output provided by the stateless API implementation, and collects + them into a performance statistics data structure. + + Attributes: + packet: The packet being used for the performance test. + send_mpps: the MPPS send rate. + duration: The duration of the test. + """ + # Convert to a dictionary. + stats_output = eval(self._generate_traffic(packet, send_mpps, duration)) + + global_output = stats_output.get("global", "ERROR - DATA NOT FOUND") + + self._logger.info(f"The global stats for the current set of params are: {global_output}") + + return PerformanceTrafficStats( + frame_size=len(packet), + tx_pps=global_output.get("tx_pps", "ERROR - tx_pps NOT FOUND"), + tx_cps=global_output.get("tx_cps", "ERROR - tx_cps NOT FOUND"), + tx_bps=global_output.get("tx_bps", "ERROR - tx_bps NOT FOUND"), + rx_pps=global_output.get("rx_pps", "ERROR - rx_pps NOT FOUND"), + rx_bps=global_output.get("rx_bps", "ERROR - rx_bps NOT FOUND"), + ) + + def _generate_traffic(self, packet: Packet, send_mpps: int, duration: float) -> str: + """Generate traffic using provided packet. + + Uses the provided packet to generate traffic for the provided duration. + + Attributes: + packet: The packet being used for the performance test. + send_mpps: MPPS send rate. + duration: The duration of the test being performed. + + Returns: + a string output of statistics provided by the traffic generator. + """ + self._create_packet_stream(packet) + self._setup_trex_client() + + stats = self._send_traffic_and_get_stats(send_mpps, duration) + + return stats + + def _setup_trex_client(self) -> None: + """Create trex client and connect to the server process.""" + # Prepare TREX client for next performance test. + procedure = [ + f"{self.stl_client_name}.connect()", + f"{self.stl_client_name}.reset(ports = [0, 1])", + f"{self.stl_client_name}.clear_stats()", + f"{self.stl_client_name}.add_streams({self.packet_stream_name}, ports=[0, 1])", + ] + + for command in procedure: + self._shell.send_command(command) + + def _create_packet_stream(self, packet: Packet) -> None: + """Create TREX packet stream with the given packet. + + Attributes: + packet: The packet being used for the performance test. + + """ + streaming_mode = "" + if self._streaming_mode == TrexStatelessTXModes.CONTINUOUS: + streaming_mode = "STLTXCont" + elif self._streaming_mode == TrexStatelessTXModes.SINGLE_BURST: + streaming_mode = "STLTXSingleBurst" + elif self._streaming_mode == TrexStatelessTXModes.MULTI_BURST: + streaming_mode = "STLTXMultiBurst" + + # Create the tx packet on the TG shell + self._shell.send_command(f"packet={packet.command()}") + + packet_stream = [ + f"{self.packet_stream_name} = trex.stl.trex_stl_streams.STLStream(", + f"name='Test_{len(packet)}_bytes',", + "packet=trex.stl.trex_stl_packet_builder_scapy.STLPktBuilder(pkt=packet),", + f"mode=trex.stl.trex_stl_streams.{streaming_mode}(percentage=100),", + ")", + ] + self._shell.send_command("\n".join(packet_stream)) + + def _send_traffic_and_get_stats(self, send_mpps: float, duration: float) -> str: + """Send traffic and get TG Rx stats. + + Sends traffic from the TREX client's ports for the given duration. + When the traffic sending duration has passed, collect the aggregate + statistics and return TREX's global stats as a string. + + Attributes: + send_mpps: The millions of packets per second for TREX to send from each port. + duration: The traffic generation duration. + """ + mpps_send_rate = f"{send_mpps}mpps" + + self._shell.send_command(f"""{self.stl_client_name}.start(ports=[0, 1], + mult = '{mpps_send_rate}', + duration = {duration})""") + + time.sleep(duration) + + stats = self._shell.send_command( + f"{self.stl_client_name}.get_stats(ports=[0, 1])", skip_first_line=True + ) + + self._shell.send_command(f"{self.stl_client_name}.stop(ports=[0, 1])") + + return stats + + def close(self) -> None: + """Overrides :meth:`.traffic_generator.TrafficGenerator.close`. + + Stops the traffic generator and sniffer shells. + """ + self._shell.close() -- 2.49.0 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [PATCH v3 4/5] dts: add trex traffic generator to dts framework 2025-07-02 5:21 ` [PATCH v3 4/5] dts: add trex traffic generator to dts framework Patrick Robb @ 2025-07-02 16:32 ` Luca Vizzarro 0 siblings, 0 replies; 35+ messages in thread From: Luca Vizzarro @ 2025-07-02 16:32 UTC (permalink / raw) To: Patrick Robb, Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, thomas.wilks, Nicholas Pratte On 02/07/2025 06:21, Patrick Robb wrote: > class TrafficGeneratorConfig(FrozenModel): > @@ -404,6 +406,8 @@ class TrafficGeneratorConfig(FrozenModel): > #: The traffic generator type the child class is required to define to be distinguished among > #: others. > type: TrafficGeneratorType > + remote_path: PurePath > + config: PurePath What's the reason behind these? And why are we making these mandatory for all of the traffic generator configurations? Given Scapy hasn't needed them so far, I reckon these shouldn't be here. > #: The DPDK configuration used to test. > dpdk: DPDKConfiguration > - #: The traffic generator configuration used to test. > - traffic_generator: TrafficGeneratorConfigTypes > + #: The traffic generator configuration used for functional tests. > + func_traffic_generator: TrafficGeneratorConfig > + #: The traffic generator configuration used for performance tests. > + perf_traffic_generator: TrafficGeneratorConfig This is starting to look like it should be optional and not mandatory. Should have a choice not to supply a perf traffic generator, same for a functional one. If I care only about perf tests, I shouldn't be needing to supply functional ones. > diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py > index 10a5e1a6b8..ea8ca41414 100644 > --- a/dts/framework/test_run.py > +++ b/dts/framework/test_run.py > @@ -204,10 +204,18 @@ def __init__( > > dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node) > dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env) > - traffic_generator = create_traffic_generator(config.traffic_generator, tg_node) > + if config.func: > + traffic_generator = create_traffic_generator(config.func_traffic_generator, tg_node) > + if config.perf: > + traffic_generator = create_traffic_generator(config.perf_traffic_generator, tg_node) This is insinuating we can only have a functional or a performance traffic generator, which does not respect the expectations of the configuration. Moreover, func and perf are not mutually exclusive, if we have both set to True this looks like it will wreak havoc. Looks like a bug to me. > diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py > index e5fbadd1a1..75d7a6eb6c 100644 > --- a/dts/framework/test_suite.py > +++ b/dts/framework/test_suite.py > @@ -254,11 +254,11 @@ def send_packets_and_capture( > A list of received packets. > """ > assert isinstance( > - self._ctx.tg, CapturingTrafficGenerator > + self._ctx.func_tg, CapturingTrafficGenerator I am evermore confused, I don't see any func_tg in Context. > diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py > index 2a259a6e6c..53125995cd 100644 > @@ -37,8 +47,10 @@ def create_traffic_generator( > Raises: > ConfigurationError: If an unknown traffic generator has been setup. > """ > - match traffic_generator_config: > - case ScapyTrafficGeneratorConfig(): > + match traffic_generator_config.type: > + case TrafficGeneratorType.SCAPY: > return ScapyTrafficGenerator(node, traffic_generator_config, privileged=True) > + case TrafficGeneratorType.TREX: > + return TrexTrafficGenerator(node, traffic_generator_config, privileged=True) > case _: > raise ConfigurationError(f"Unknown traffic generator: {traffic_generator_config.type}") This change defeats the entire purpose of the match case. This was originally doing two things: - matching traffic generator by their own config class - casting the config class (this is not happening anymore) The traffic generator classes should be able to access their own configurations directly, this is basically breaking the whole purpose of mypy. > diff --git a/dts/framework/testbed_model/traffic_generator/trex.py b/dts/framework/testbed_model/traffic_generator/trex.py > new file mode 100644 > index 0000000000..1ba7d6bbbd > --- /dev/null > +++ b/dts/framework/testbed_model/traffic_generator/trex.py > @@ -0,0 +1,258 @@ > +"""Implementation for TREX performance traffic generator.""" Missing SPDX License Identifier and copyrights. For such a big file, I'd have expected a description for the implementation and how to configure TRex in DTS. > +class TrexStatelessTXModes(Flag): > + """Flags indicating TREX instance's current transmission mode.""" > + > + CONTINUOUS = auto() > + SINGLE_BURST = auto() > + MULTI_BURST = auto() Missing docstrings. > +class TrexTrafficGenerator(PerformanceTrafficGenerator): > + """TREX traffic generator. > + > + This implementation leverages the stateless API library provided in the TREX installation. > + > + Attributes: > + stl_client_name: The name of the stateless client used in the stateless API. > + packet_stream_name: The name of the stateless packet stream used in the stateless API. > + """ > + > + _os_session: OSSession > + _server_remote_session: Any Why are we resorting to Any? > + > + _tg_config: TrafficGeneratorConfig > + _node_config: NodeConfiguration > + > + _shell: PythonShell > + _python_indentation: ClassVar[str] = " " * 4 > + > + stl_client_name: ClassVar[str] = "client" > + packet_stream_name: ClassVar[str] = "stream" > + > + _streaming_mode: TrexStatelessTXModes = TrexStatelessTXModes.CONTINUOUS > + > + tg_cores: int = 10 This is not documented in the docstring. > + > + def __init__(self, tg_node: Node, config: TrafficGeneratorConfig, **kwargs) -> None: > + """Initialize the TREX server. > + > + Initializes needed OS sessions for the creation of the TREX server process. > + > + Attributes: > + tg_node: TG node the TREX instance is operating on. > + config: Traffic generator config provided for TREX instance. > + """ I think the above is meant to be Args. > + super().__init__(tg_node=tg_node, config=config, **kwargs) > + self._tg_node_config = tg_node.config > + self._tg_config = config > + > + self._os_session = create_session(self._tg_node.config, "TREX", self._logger) > + self._server_remote_session = self._os_session.remote_session > + > + self._shell = PythonShell(self._tg_node, "TREX-client", privileged=True) > + > + def setup(self, topology: Topology): > + """Initialize and start a TREX server process.""" > + super().setup(get_ctx().topology) Why are we calling topology from get_ctx() if we are accepting it from the arguments? > + # Start TREX server process. > + try: > + self._logger.info("Starting TREX server process: sending 20 second sleep.") > + server_command = [ > + f"cd {self._tg_config.remote_path};", > + self._os_session._get_privileged_command( > + f"screen -d -m ./t-rex-64 --cfg {self._tg_config.config} -c {self.tg_cores} -i" > + ), > + ] > + privileged_command = " ".join(server_command) > + self._logger.info(f"Sending: '{privileged_command}") > + self._server_remote_session.session.run(privileged_command) > + time.sleep(20) Looks kind of hacky and slow, is there no way to determine if the process is ready? Would something like the BlockingDPDKApp class work here? (Of course in a generic way, like the patches I am about to send) > + > + except SSHTimeoutError as e: > + self._logger.exception("Failed to start TREX server process.", e) > + > + # Start Python shell. > + self._shell.start_application() > + self._shell.send_command("import os") > + self._shell.send_command( > + f"os.chdir('{self._tg_config.remote_path}/automation/trex_control_plane/interactive')" > + ) I would properly join the path first use PurePath. > + > + # Import stateless API components. > + imports = [ > + "import trex", > + "import trex.stl", > + "import trex.stl.trex_stl_client", > + "import trex.stl.trex_stl_streams", > + "import trex.stl.trex_stl_packet_builder_scapy", > + "from scapy.layers.l2 import Ether", > + "from scapy.layers.inet import IP", > + "from scapy.packet import Raw", > + ] > + self._shell.send_command("\n".join(imports)) > + > + stateless_client = [ > + f"{self.stl_client_name} = trex.stl.trex_stl_client.STLClient(", > + f"username='{self._tg_node_config.user}',", > + "server='127.0.0.1',", > + ")", > + ] > + > + self._shell.send_command(f"\n{self._python_indentation}".join(stateless_client)) > + self._shell.send_command(f"{self.stl_client_name}.connect()") > + > + def teardown(self) -> None: > + """Teardown the TREX server and stateless implementation. > + > + close the TREX server process, and stop the Python shell. Missed capitalisation. > + > + Attributes: > + ports: Associated ports used by the TREX instance. > + """ What attributes? > + super().teardown() > + self._os_session.send_command("pkill t-rex-64", privileged=True) > + self.close() > + > + def calculate_traffic_and_stats( > + self, packet: Packet, send_mpps: int, duration: float > + ) -> PerformanceTrafficStats: > + """Calculate the traffic statistics, using provided TG output. > + > + Takes in the statistics output provided by the stateless API implementation, and collects > + them into a performance statistics data structure. > + > + Attributes: > + packet: The packet being used for the performance test. > + send_mpps: the MPPS send rate. > + duration: The duration of the test. > + """ Args, not Attributes. > + # Convert to a dictionary. > + stats_output = eval(self._generate_traffic(packet, send_mpps, duration)) I am not really pleased with the idea of injecting arbitrary code, this screams security issue. Is there no better way to handle this? We want to parse and validate output, not just evaluate it blindly. Could we get this as JSON and let a Pydantic model naturally parse it and validate it safely? > + > + global_output = stats_output.get("global", "ERROR - DATA NOT FOUND") > + > + self._logger.info(f"The global stats for the current set of params are: {global_output}") > + > + return PerformanceTrafficStats( > + frame_size=len(packet), > + tx_pps=global_output.get("tx_pps", "ERROR - tx_pps NOT FOUND"), > + tx_cps=global_output.get("tx_cps", "ERROR - tx_cps NOT FOUND"), > + tx_bps=global_output.get("tx_bps", "ERROR - tx_bps NOT FOUND"), > + rx_pps=global_output.get("rx_pps", "ERROR - rx_pps NOT FOUND"), > + rx_bps=global_output.get("rx_bps", "ERROR - rx_bps NOT FOUND"), > + ) Otherwise, PerformanceTrafficStats could just re-use the TextParser class for this purpose as well. > + > + def _generate_traffic(self, packet: Packet, send_mpps: int, duration: float) -> str: > + """Generate traffic using provided packet. > + > + Uses the provided packet to generate traffic for the provided duration. > + > + Attributes: > + packet: The packet being used for the performance test. > + send_mpps: MPPS send rate. > + duration: The duration of the test being performed. Args, not Attributes. > + > + Returns: > + a string output of statistics provided by the traffic generator. Missed capitalisation. > + """ > + self._create_packet_stream(packet) > + self._setup_trex_client() > + > + stats = self._send_traffic_and_get_stats(send_mpps, duration) > + > + return stats > + > + def _setup_trex_client(self) -> None: > + """Create trex client and connect to the server process.""" > + # Prepare TREX client for next performance test. > + procedure = [ > + f"{self.stl_client_name}.connect()", > + f"{self.stl_client_name}.reset(ports = [0, 1])", > + f"{self.stl_client_name}.clear_stats()", > + f"{self.stl_client_name}.add_streams({self.packet_stream_name}, ports=[0, 1])", > + ] > + > + for command in procedure: > + self._shell.send_command(command) > + > + def _create_packet_stream(self, packet: Packet) -> None: > + """Create TREX packet stream with the given packet. > + > + Attributes: > + packet: The packet being used for the performance test. > + empty line > + """ > + streaming_mode = "" > + if self._streaming_mode == TrexStatelessTXModes.CONTINUOUS: > + streaming_mode = "STLTXCont" > + elif self._streaming_mode == TrexStatelessTXModes.SINGLE_BURST: > + streaming_mode = "STLTXSingleBurst" > + elif self._streaming_mode == TrexStatelessTXModes.MULTI_BURST: > + streaming_mode = "STLTXMultiBurst" TrexStatelessTXModes could just be a StrEnum with these as values. > + > + # Create the tx packet on the TG shell > + self._shell.send_command(f"packet={packet.command()}") > + > + packet_stream = [ > + f"{self.packet_stream_name} = trex.stl.trex_stl_streams.STLStream(", > + f"name='Test_{len(packet)}_bytes',", > + "packet=trex.stl.trex_stl_packet_builder_scapy.STLPktBuilder(pkt=packet),", > + f"mode=trex.stl.trex_stl_streams.{streaming_mode}(percentage=100),", > + ")", > + ] > + self._shell.send_command("\n".join(packet_stream)) > + > + def _send_traffic_and_get_stats(self, send_mpps: float, duration: float) -> str: > + """Send traffic and get TG Rx stats. > + > + Sends traffic from the TREX client's ports for the given duration. > + When the traffic sending duration has passed, collect the aggregate > + statistics and return TREX's global stats as a string. > + > + Attributes: > + send_mpps: The millions of packets per second for TREX to send from each port. > + duration: The traffic generation duration. Args, not Attributes. > + """ > + mpps_send_rate = f"{send_mpps}mpps" > + > + self._shell.send_command(f"""{self.stl_client_name}.start(ports=[0, 1], > + mult = '{mpps_send_rate}', > + duration = {duration})""") bad formatting. > + > + time.sleep(duration) > + > + stats = self._shell.send_command( > + f"{self.stl_client_name}.get_stats(ports=[0, 1])", skip_first_line=True > + ) > + > + self._shell.send_command(f"{self.stl_client_name}.stop(ports=[0, 1])") > + > + return stats > + > + def close(self) -> None: > + """Overrides :meth:`.traffic_generator.TrafficGenerator.close`. > + > + Stops the traffic generator and sniffer shells. > + """ > + self._shell.close() ^ permalink raw reply [flat|nested] 35+ messages in thread
* [PATCH v3 5/5] dts: add performance test functions to test suite API 2025-07-02 5:21 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Patrick Robb ` (2 preceding siblings ...) 2025-07-02 5:21 ` [PATCH v3 4/5] dts: add trex traffic generator to dts framework Patrick Robb @ 2025-07-02 5:21 ` Patrick Robb 2025-07-02 16:37 ` Luca Vizzarro 2025-07-02 15:09 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Luca Vizzarro 4 siblings, 1 reply; 35+ messages in thread From: Patrick Robb @ 2025-07-02 5:21 UTC (permalink / raw) To: Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, Luca.Vizzarro, thomas.wilks, Nicholas Pratte, Patrick Robb From: Nicholas Pratte <npratte@iol.unh.edu> Provide functional performance method to run performance tests using a user-supplied performance traffic generator. The single core performance test is included, with some basic statistics checks verifying TG packet transmission rates. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> Signed-off-by: Patrick Robb <probb@iol.unh.edu> Reviewed-by: Dean Marx <dmarx@iol.unh.edu> --- dts/configurations/tests_config.example.yaml | 12 +++++++ dts/framework/test_suite.py | 34 ++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/dts/configurations/tests_config.example.yaml b/dts/configurations/tests_config.example.yaml index c011ac0588..c56951b2b0 100644 --- a/dts/configurations/tests_config.example.yaml +++ b/dts/configurations/tests_config.example.yaml @@ -3,3 +3,15 @@ # Define the custom test suite configurations hello_world: msg: A custom hello world to you! +single_core_forward_perf: + test_parameters: + - frame_size: 64 + num_descriptors: 128 + expected_mpps: 30.1234 + - frame_size: 64 + num_descriptors: 128 + expected_mpps: 30.2341 + - frame_size: 512 + num_descriptors: 128 + expected_mpps: 19.453376 + delta_tolerance: 0.05 \ No newline at end of file diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py index 75d7a6eb6c..4044c85d4d 100644 --- a/dts/framework/test_suite.py +++ b/dts/framework/test_suite.py @@ -38,6 +38,10 @@ CapturingTrafficGenerator, PacketFilteringConfig, ) +from framework.testbed_model.traffic_generator.performance_traffic_generator import ( + PerformanceTrafficGenerator, + PerformanceTrafficStats, +) from .exception import ConfigurationError, InternalError, TestCaseVerifyError from .logger import DTSLogger, get_dts_logger @@ -254,11 +258,11 @@ def send_packets_and_capture( A list of received packets. """ assert isinstance( - self._ctx.func_tg, CapturingTrafficGenerator + self._ctx.tg, CapturingTrafficGenerator ), "Cannot capture with a non-capturing traffic generator" # TODO: implement @requires for types of traffic generator packets = self._adjust_addresses(packets) - return self._ctx.func_tg.send_packets_and_capture( + return self._ctx.tg.send_packets_and_capture( packets, self._ctx.topology.tg_port_egress, self._ctx.topology.tg_port_ingress, @@ -266,6 +270,27 @@ def send_packets_and_capture( duration, ) + def assess_performance_by_packet( + self, packet: Packet, send_mpps: int, duration: int = 60 + ) -> PerformanceTrafficStats: + """Send a given packet for a given duration and assess basic performance statistics. + + Send `packet` and assess NIC performance for a given duration, corresponding to the test + suite's given topology. + + Args: + packet: The packet to send. + send_mpps: The millions packets per second send rate. + duration: Performance test duration (in seconds). + + Returns: + Performance statistics of the generated test. + """ + assert isinstance( + self._ctx.tg, PerformanceTrafficGenerator + ), "Cannot run performance tests on non-performance traffic generator." + return self._ctx.tg.calculate_traffic_and_stats(packet, send_mpps, duration) + def send_packets( self, packets: list[Packet], @@ -275,8 +300,11 @@ def send_packets( Args: packets: Packets to send. """ + assert isinstance( + self._ctx.tg, CapturingTrafficGenerator + ), "Cannot run performance tests on non-capturing traffic generator." packets = self._adjust_addresses(packets) - self._ctx.func_tg.send_packets(packets, self._ctx.topology.tg_port_egress) + self._ctx.tg.send_packets(packets, self._ctx.topology.tg_port_egress) def get_expected_packets( self, -- 2.49.0 ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [PATCH v3 5/5] dts: add performance test functions to test suite API 2025-07-02 5:21 ` [PATCH v3 5/5] dts: add performance test functions to test suite API Patrick Robb @ 2025-07-02 16:37 ` Luca Vizzarro 0 siblings, 0 replies; 35+ messages in thread From: Luca Vizzarro @ 2025-07-02 16:37 UTC (permalink / raw) To: Patrick Robb, Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, thomas.wilks, Nicholas Pratte On 02/07/2025 06:21, Patrick Robb wrote: > From: Nicholas Pratte <npratte@iol.unh.edu> > > Provide functional performance method to run performance tests using a > user-supplied performance traffic generator. The single core performance > test is included, with some basic statistics checks verifying TG packet is the test missing? > transmission rates. > > Bugzilla ID: 1697 > Signed-off-by: Nicholas Pratte <npratte@iol.unh.edu> > Signed-off-by: Patrick Robb <probb@iol.unh.edu> > Reviewed-by: Dean Marx <dmarx@iol.unh.edu> > --- > dts/configurations/tests_config.example.yaml | 12 +++++++ > dts/framework/test_suite.py | 34 ++++++++++++++++++-- > 2 files changed, 43 insertions(+), 3 deletions(-) > > diff --git a/dts/configurations/tests_config.example.yaml b/dts/configurations/tests_config.example.yaml > index c011ac0588..c56951b2b0 100644 > --- a/dts/configurations/tests_config.example.yaml > +++ b/dts/configurations/tests_config.example.yaml > @@ -3,3 +3,15 @@ > # Define the custom test suite configurations > hello_world: > msg: A custom hello world to you! > +single_core_forward_perf: > + test_parameters: > + - frame_size: 64 > + num_descriptors: 128 > + expected_mpps: 30.1234 > + - frame_size: 64 > + num_descriptors: 128 > + expected_mpps: 30.2341 > + - frame_size: 512 > + num_descriptors: 128 > + expected_mpps: 19.453376 > + delta_tolerance: 0.05 where does this come from? Is it meant for the missing test suite? > \ No newline at end of file > diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py > index 75d7a6eb6c..4044c85d4d 100644 > --- a/dts/framework/test_suite.py > +++ b/dts/framework/test_suite.py > @@ -38,6 +38,10 @@ > CapturingTrafficGenerator, > PacketFilteringConfig, > ) > +from framework.testbed_model.traffic_generator.performance_traffic_generator import ( > + PerformanceTrafficGenerator, > + PerformanceTrafficStats, > +) > > from .exception import ConfigurationError, InternalError, TestCaseVerifyError > from .logger import DTSLogger, get_dts_logger > @@ -254,11 +258,11 @@ def send_packets_and_capture( > A list of received packets. > """ > assert isinstance( > - self._ctx.func_tg, CapturingTrafficGenerator > + self._ctx.tg, CapturingTrafficGenerator This looks like it's fixing an issue I noted earlier in the review, should be squashed. > ), "Cannot capture with a non-capturing traffic generator" > # TODO: implement @requires for types of traffic generator > packets = self._adjust_addresses(packets) > - return self._ctx.func_tg.send_packets_and_capture( > + return self._ctx.tg.send_packets_and_capture( > packets, > self._ctx.topology.tg_port_egress, > self._ctx.topology.tg_port_ingress, > @@ -266,6 +270,27 @@ def send_packets_and_capture( > duration, > ) > > + def assess_performance_by_packet( > + self, packet: Packet, send_mpps: int, duration: int = 60 > + ) -> PerformanceTrafficStats: > + """Send a given packet for a given duration and assess basic performance statistics. > + > + Send `packet` and assess NIC performance for a given duration, corresponding to the test > + suite's given topology. > + > + Args: > + packet: The packet to send. > + send_mpps: The millions packets per second send rate. > + duration: Performance test duration (in seconds). > + > + Returns: > + Performance statistics of the generated test. > + """ > + assert isinstance( > + self._ctx.tg, PerformanceTrafficGenerator > + ), "Cannot run performance tests on non-performance traffic generator." > + return self._ctx.tg.calculate_traffic_and_stats(packet, send_mpps, duration) > + > def send_packets( > self, > packets: list[Packet], > @@ -275,8 +300,11 @@ def send_packets( > Args: > packets: Packets to send. > """ > + assert isinstance( > + self._ctx.tg, CapturingTrafficGenerator > + ), "Cannot run performance tests on non-capturing traffic generator." I am still unsure if we are meant to have co-existing traffic generators. But we should make everything more tolerant, ideally this should never be reached, and DTS should automatically skip tests appropriately. The above assertion and message don't really appear related, so this should also be rectified. > packets = self._adjust_addresses(packets) > - self._ctx.func_tg.send_packets(packets, self._ctx.topology.tg_port_egress) > + self._ctx.tg.send_packets(packets, self._ctx.topology.tg_port_egress) > > def get_expected_packets( > self, ^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [PATCH v3 1/5] dts: rework config module to support perf TGs 2025-07-02 5:21 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Patrick Robb ` (3 preceding siblings ...) 2025-07-02 5:21 ` [PATCH v3 5/5] dts: add performance test functions to test suite API Patrick Robb @ 2025-07-02 15:09 ` Luca Vizzarro 4 siblings, 0 replies; 35+ messages in thread From: Luca Vizzarro @ 2025-07-02 15:09 UTC (permalink / raw) To: Patrick Robb, Paul.Szczepanek Cc: dev, dmarx, nprattedev, mmahajan, abailey, thomas.wilks, Nicholas Pratte Hi Patrick and Nick, I am unsure what happened with this commit. The commit subject doesn't match what's happening here. Mentions a rework of the config module, which is not. Description seems ok. Another issue I see is that some changes don't make a lot logical sense as they stand here. The changes in test_run.yaml, don't really belong here, and could be considered breaking. I am assuming they were improperly re-ordered between commits. Similarly I don't see appropriate doc changes to reflect the change in the configuration examples path. On 02/07/2025 06:21, Patrick Robb wrote: > --- a/dts/test_run.example.yaml > +++ b/dts/configurations/test_run.example.yaml > @@ -1,8 +1,3 @@ > -# SPDX-License-Identifier: BSD-3-Clause > -# Copyright 2022-2023 The DPDK contributors > -# Copyright 2023 Arm Limited > - > -# Define the test run environment And removing this line above, would cause in sphinx to complain as the example rendering relies on this line. > dpdk: > lcores: "" # use all available logical cores (Skips first core) > memory_channels: 4 # tells DPDK to use 4 memory channels > @@ -23,8 +18,14 @@ dpdk: > # in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options` > # to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be > # defined, but not both. ^ permalink raw reply [flat|nested] 35+ messages in thread
end of thread, other threads:[~2025-07-02 16:38 UTC | newest] Thread overview: 35+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2025-04-23 19:40 [RFC Patch v1 0/5] Add TREX Traffic Generator to DTS Framework Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 1/5] dts: rework config module to support perf TGs Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 2/5] dts: rework traffic generator inheritance structure Nicholas Pratte 2025-05-15 19:24 ` Patrick Robb 2025-05-16 19:12 ` Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 3/5] dts: add asychronous support to ssh sessions Nicholas Pratte 2025-05-15 19:24 ` Patrick Robb 2025-04-23 19:40 ` [RFC Patch v1 4/5] dts: add trex traffic generator to dts framework Nicholas Pratte 2025-05-15 19:25 ` Patrick Robb 2025-05-16 19:45 ` Nicholas Pratte 2025-04-23 19:40 ` [RFC Patch v1 5/5] dts: add performance test functions to test suite api Nicholas Pratte 2025-05-15 19:25 ` Patrick Robb 2025-05-16 20:18 ` [RFC v2 0/6] Add TREX Traffic Generator to DTS Framework Nicholas Pratte 2025-05-16 20:18 ` [RFC v2 1/6] dts: rework config module to support perf TGs Nicholas Pratte 2025-05-20 20:33 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 2/6] dts: rework traffic generator inheritance structure Nicholas Pratte 2025-05-21 20:36 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 3/6] dts: add asynchronous support to ssh sessions Nicholas Pratte 2025-05-22 15:04 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 4/6] dts: add extended timeout option to interactive shells Nicholas Pratte 2025-05-22 15:10 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 5/6] dts: add trex traffic generator to dts framework Nicholas Pratte 2025-05-22 16:55 ` Dean Marx 2025-05-16 20:18 ` [RFC v2 6/6] dts: add performance test functions to test suite api Nicholas Pratte 2025-05-22 17:54 ` Dean Marx 2025-07-02 5:21 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Patrick Robb 2025-07-02 5:21 ` [PATCH v3 2/5] dts: rework traffic generator inheritance structure Patrick Robb 2025-07-02 15:31 ` Luca Vizzarro 2025-07-02 5:21 ` [PATCH v3 3/5] dts: add timeout override option to interactive shells Patrick Robb 2025-07-02 15:33 ` Luca Vizzarro 2025-07-02 5:21 ` [PATCH v3 4/5] dts: add trex traffic generator to dts framework Patrick Robb 2025-07-02 16:32 ` Luca Vizzarro 2025-07-02 5:21 ` [PATCH v3 5/5] dts: add performance test functions to test suite API Patrick Robb 2025-07-02 16:37 ` Luca Vizzarro 2025-07-02 15:09 ` [PATCH v3 1/5] dts: rework config module to support perf TGs Luca Vizzarro
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for NNTP newsgroup(s).