On 03/11/2026 22:02, Koushik Bhargav Nimoji wrote:
diff --git a/dts/api/testbed_model/virtual_device.py 
b/dts/api/testbed_model/virtual_device.py
index 1a4794e695..8ac9a625f5 100644
--- a/dts/api/testbed_model/virtual_device.py
+++ b/dts/api/testbed_model/virtual_device.py
@@ -6,6 +6,8 @@
  Alongside support for physical hardware, DPDK can create various virtual 
devices.
  """
+from api.context import get_ctx
+
class VirtualDevice:
      """Base class for virtual devices used by DPDK.
@@ -27,3 +29,9 @@ def __init__(self, name: str) -> None:
      def __str__(self) -> str:
          """This corresponds to the name used for DPDK devices."""
          return self.name
+
+
+def clean_up_vhost() -> None:
+    """Deletes stale vhost-user Unix domain socket files."""
+    ctx = get_ctx()
+    ctx.sut_node.main_session.send_command("rm -rf /tmp/vhost-net*", 
privileged=True)
I welcome this change but send_command is still something that should be called by the relevant OSSession class. In theory main_session can be a session for a host of any OS, be Linux, FreeBSD, Windows etc. Therefore the changes should be OS-independent.

I've reviewed the usage better and the test suite shouldn't be able to arbitrarily create files out of arbitrary paths. We have the class Artifact to help abstract the remote filesystem away. On remote session this also takes care of cleanup.

Instead of introducing this change which has no real ownership in the framework and api packages, I'd suggest letting the test suite create the vhost-net using the Artifact class instead.
diff --git a/dts/configurations/tests_config.example.yaml 
b/dts/configurations/tests_config.example.yaml
index ce67385b0b..36a0e7c2f1 100644
--- a/dts/configurations/tests_config.example.yaml
+++ b/dts/configurations/tests_config.example.yaml
@@ -18,6 +18,30 @@ hello_world:
  #       num_descriptors: 1024
  #       expected_mpps: 1.0
  #   delta_tolerance: 0.05
+# pvp_multi_paths_vhost_single_core_perf:
+#   test_parameters: # Add frame size / descriptor count combinations as needed
+#     - frame_size: 64
+#       num_descriptors: 512
+#       expected_mpps: 1.0 # Set millions of packets per second according to 
your devices expected throughput for this given frame size / descriptor count
+#     - frame_size: 64
+#       num_descriptors: 1024
+#       expected_mpps: 1.0
+#     - frame_size: 512
+#       num_descriptors: 1024
+#       expected_mpps: 1.0
+#   delta_tolerance: 0.05
+# pvp_multi_paths_virtio_single_core_perf:
+#   test_parameters: # Add frame size / descriptor count combinations as needed
+#     - frame_size: 64
+#       num_descriptors: 512
+#       expected_mpps: 1.0 # Set millions of packets per second according to 
your devices expected throughput for this given frame size / descriptor count
+#     - frame_size: 64
+#       num_descriptors: 1024
+#       expected_mpps: 1.0
+#     - frame_size: 512
+#       num_descriptors: 1024
+#       expected_mpps: 1.0
+#   delta_tolerance: 0.05
  # pmd_rss:
  #   NUM_QUEUES: 4
  #   ACTUAL_KEY_SIZE: 52
diff --git a/dts/tests/TestSuite_pvp_multi_paths_vhost_single_core_perf.py 
b/dts/tests/TestSuite_pvp_multi_paths_vhost_single_core_perf.py
new file mode 100644
index 0000000000..269fad1782
--- /dev/null
+++ b/dts/tests/TestSuite_pvp_multi_paths_vhost_single_core_perf.py
@@ -0,0 +1,523 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2026 University of New Hampshire
+
+"""Vhost-user single-core forwarding performance test suite.
+
+This suite measures the packet forwarding performance of DPDK's vhost-user PMD 
running on a
+single CPU core in a Physical-Virtual-Physical (PVP) topology. Performance is 
evaluated across
+multiple virtio-user backend options, including packed versus split ring 
formats, in-order packet
+processing, mergeable buffers, and SIMD vectorization. Test cases are 
parameterized using
+combinations of frame sizes, descriptor ring sizes, and target MPPS baselines 
specified in the
+test configuration. High-speed traffic is injected from an external generator 
through SUT physical
+interfaces, routed through Vhost and Virtio loops, and returned to the 
generator to measure
+aggregate MPPS.
+"""
+
+from typing import TypedDict, cast
+
+from scapy.layers.inet import IP
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from typing_extensions import NotRequired
+
+from api.capabilities import (
+    LinkTopology,
+    requires_link_topology,
+)
+from api.packet import assess_performance_by_packet
+from api.params.eal import VirtualDevice
+from api.params.types import LogicalCoreList, RSSSetting, TestPmdParamsDict
+from api.test import get_current_test_case_name, verify, write_performance_json
+from api.test_suite import BaseConfig, TestSuite, perf_suite, perf_test
+from api.testbed_model.virtual_device import clean_up_vhost
+from api.testpmd import TestPmd
+from api.testpmd.config import (
+    Params,
+    PortTopology,
+    RXRingParams,
+    SimpleForwardingModes,
+    TXRingParams,
+)
+
+
+class TestParameter(TypedDict):
+    """Performance test metrics."""
+
+    frame_size: int
+    num_descriptors: int
+    expected_mpps: float
+    performance_delta: NotRequired[float | None]
+    measured_mpps: NotRequired[float | None]
+    passing: NotRequired[float | None]
I've noticed that this kind of configuration is ubiquitous across performance test suites. I'd propose to abstract the Config and this away as a generic PerfSuiteConfig and re-use it accordingly. In that case you can move away from using a TypedDict and use a Pydantic BaseConfig instead which will allow you to use more powerful constructs.

You won't need to use NotRequired – which I am assuming makes doing `| None` redundant, in that case. Moreover the fields should be documented appropriately similarly to what was done in the config package.> +
+
+class Config(BaseConfig):
+    """Test configuration for pvp_multi_paths_vhost_single_core_perf test 
suite."""
+
+    test_parameters: list[TestParameter] = [
+        {
+            "frame_size": 64,
+            "num_descriptors": 1024,
+            "expected_mpps": 1.00,
+            "performance_delta": None,
+            "measured_mpps": None,
+            "passing": None,
+        },
+        {
+            "frame_size": 128,
+            "num_descriptors": 1024,
+            "expected_mpps": 1.00,
+            "performance_delta": None,
+            "measured_mpps": None,
+            "passing": None,
+        },
+        {
+            "frame_size": 256,
+            "num_descriptors": 1024,
+            "expected_mpps": 1.00,
+            "performance_delta": None,
+            "measured_mpps": None,
+            "passing": None,
+        },
+        {
+            "frame_size": 512,
+            "num_descriptors": 1024,
+            "expected_mpps": 1.00,
+            "performance_delta": None,
+            "measured_mpps": None,
+            "passing": None,
+        },
+        {
+            "frame_size": 1024,
+            "num_descriptors": 1024,
+            "expected_mpps": 1.00,
+            "performance_delta": None,
+            "measured_mpps": None,
+            "passing": None,
+        },
+        {
+            "frame_size": 1518,
+            "num_descriptors": 1024,
+            "expected_mpps": 1.00,
+            "performance_delta": None,
+            "measured_mpps": None,
+            "passing": None,
+        },
+    ]
+    delta_tolerance: float = 0.05
So all of the above could be a common Config class that can be re-used across perf test suites.> +
+
+@perf_suite
+@requires_link_topology(LinkTopology.TWO_LINKS)
+class TestPvpMultiPathsVhostSingleCorePerf(TestSuite):
+    """pvp multi paths vhost single core performance test suite."""
+
+    config: Config
+
+    def set_up_test_case(self) -> None:
+        """Delete stale vhost-user Unix domain socket files."""
+        clean_up_vhost()
+
+    def _transmit(
+        self, vhost: TestPmd, virtio: TestPmd, frame_size: int, repetitions: 
int = 1
+    ) -> float:
+        """Transmit packets using testpmd instances and compute average MPPS.
+
+        Args:
+            vhost: the running vhost testpmd shell.
+            virtio: the running virtio testpmd shell.
+            frame_size: The size of the frame to transmit.
+            repetitions: The number of times to rerun the transmission.
+
+        Returns:
+            The MPPS (millions of packets per second) forwarded by the SUT.
+        """
+        assert repetitions > 0, "Invalid number of repetitions given, must be 
greater than 0."
+        # Build packet with dummy values, and account for the 14B and 20B 
Ether and IP headers
+        packet = (
+            Ether(src="52:00:00:00:00:00")
+            / IP(src="1.2.3.4", dst="192.18.1.0")
+            / Raw(load="x" * (frame_size - 14 - 20))
+        )
+
+        vhost.start()
+        virtio.start()
+        rx_avg = 0.0
+
+        for _ in range(repetitions):
+            # Transmit for 5 seconds.
+            stats = assess_performance_by_packet(packet=packet, duration=5)
+            rx_avg += stats.rx_pps
+        return rx_avg / (repetitions * 1_000_000)
+
+    def _create_and_transmit(
+        self,
+        ring_format: int,
+        in_order: int,
+        buffers: int,
+        vectorized: int,
+        extra_args: TestPmdParamsDict,
+    ) -> None:
+        """Create testpmd instances with specified params and send traffic.
+
+        Args:
+            ring_format: selects the virtio queue descriptor layout.
+            in_order: toggles in-order packet processing.
+            buffers: enables mergeable rx buffers.
+            vectorized: enables SIMD vectorized rx/tx data paths.
+            extra_args: extra/generic TestPmd arguments.
+        """
+        sut_dpdk_driver = self._ctx.sut_node.config.ports[0].os_driver_for_dpdk
Here you should be able to do:

vhost_iface = Artifact("sut", "vhost-net", save_local_copy=False)> + vhost_user_vdev = VirtualDevice("net_vhost0,iface=/tmp/vhost-net,queues=1")
  VirtualDevice(f"net_vhost0,iface={vhost_iface.path},queues=1")

etc.
+        virtio_user_vdev = VirtualDevice(
+            "net_virtio_user0,"
+            "mac=00:11:22:33:44:10,"
+            "path=/tmp/vhost-net,"
+            "queues=1,"
+            f"packed_vq={ring_format},"
+            f"mrg_rxbuf={buffers},"
+            f"in_order={in_order},"
+            f"vectorized={vectorized}"
+        )
+
+        for params in self.config.test_parameters:
+            frame_size = params.get("frame_size")
+            num_descriptors = params.get("num_descriptors") or 1024
+            expected_mpps = params.get("expected_mpps") or 1.00
Wouldn't using the default field more appropriate here? What if the param is 0? Thinking of:

  num_descriptors = params.get("num_descriptors", 1024)
  expected_mpps = params.get("expected_mpps", 1.00)> +
+            default_args: TestPmdParamsDict = {
+                "tx_ring": TXRingParams(descriptors=int(num_descriptors)),
+                "rx_ring": RXRingParams(descriptors=int(num_descriptors)),
+            }
+
+            if sut_dpdk_driver == "mlx5_core":
+                default_args["burst"] = 64
+                default_args["mbcache"] = 512
+            elif sut_dpdk_driver == "i40e":
+                default_args["rx_queues"] = 1
+                default_args["tx_queues"] = 1
+
+            vhost_params: TestPmdParamsDict = {
+                "prefix": "vhost",
+                "allowed_ports": [],
+                "memory_channels": 4,
+                "port_topology": PortTopology.chained,
+                "vdevs": [vhost_user_vdev],
+                "lcore_list": LogicalCoreList([1, 2]),
+                "nb_cores": 1,
+                **default_args,
+            }
+
+            virtio_params: TestPmdParamsDict = {
+                "prefix": "virtio",
+                "no_pci": True,
+                "memory_channels": 4,
+                "allowed_ports": [],
+                "vdevs": [virtio_user_vdev],
+                "lcore_list": LogicalCoreList([3, 4, 5]),
+                "nb_cores": 2,
+                **extra_args,
+                **default_args,
+            }
+
+            with (
+                TestPmd(**vhost_params) as vhost,
+                TestPmd(**virtio_params) as virtio,
+            ):
+                vhost.set_forward_mode(SimpleForwardingModes.mac)
+                virtio.set_forward_mode(SimpleForwardingModes.io)
+                vhost.set_portlist([0, 2, 1])
+
+                assert expected_mpps > 0, "Invalid expected mpps, must be greater 
than 0."

Is this assertion a check for a test suite bug or an actual testing detail? If the latter then verify should be used here.

+
+                measured_mpps = round(
+                    self._transmit(vhost, virtio, int(frame_size), 
repetitions=5), 3
+                )
+
+                performance_delta = round(
+                    (float(measured_mpps) - float(expected_mpps)) / 
float(expected_mpps),
+                    3,
+                )
+
+                params["measured_mpps"] = measured_mpps
+                params["performance_delta"] = performance_delta
+                params["passing"] = float(performance_delta) >= 
-self.config.delta_tolerance
+
+        self._produce_stats_table(self.config.test_parameters)
+        for params in self.config.test_parameters:
+            verify(
+                params["passing"] is True,
+                f"""Packets forwarded is less than {(1 - 
self.config.delta_tolerance) * 100}%
+                of the expected baseline.
+                Measured MPPS = {params["measured_mpps"]}
+                Expected MPPS = {params["expected_mpps"]}""",
+            )

Reply via email to