Skip to content

Commit 400d54a

Browse files
Nicholas PrattePatrickRobbIOL
authored andcommitted
dts: add performance test functions and support
Provide packet transmission function to support performance tests using a user-supplied performance traffic generator. The single core performance test is included. It allows the user to define a matrix of frame size, descriptor count, and expected mpps, and fails if any combination does not forward a mpps count within 5% of the given baseline. Furthermore ensure that the DPDK build on the SUT is including the correct arguments to allow for the highest possible packet throughput. Bugzilla ID: 1697 Signed-off-by: Nicholas Pratte <[email protected]> Signed-off-by: Patrick Robb <[email protected]> Reviewed-by: Dean Marx <[email protected]> Reviewed-by: Andrew Bailey <[email protected]> Tested-by: Dean Marx <[email protected]>
1 parent 52564a1 commit 400d54a

File tree

6 files changed

+249
-2
lines changed

6 files changed

+249
-2
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. SPDX-License-Identifier: BSD-3-Clause
2+
3+
single_core_forward_perf Test Suite
4+
===================================
5+
6+
.. automodule:: tests.TestSuite_single_core_forward_perf
7+
:members:
8+
:show-inheritance:

dts/api/packet.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
from framework.testbed_model.traffic_generator.capturing_traffic_generator import (
3434
PacketFilteringConfig,
3535
)
36+
from framework.testbed_model.traffic_generator.performance_traffic_generator import (
37+
PerformanceTrafficStats,
38+
)
3639
from framework.utils import get_packet_summaries
3740

3841

@@ -108,7 +111,9 @@ def send_packets(
108111
packets: Packets to send.
109112
"""
110113
packets = adjust_addresses(packets)
111-
get_ctx().func_tg.send_packets(packets, get_ctx().topology.tg_port_egress)
114+
tg = get_ctx().func_tg
115+
if tg:
116+
tg.send_packets(packets, get_ctx().topology.tg_port_egress)
112117

113118

114119
def get_expected_packets(
@@ -317,3 +322,31 @@ def _verify_l3_packet(received_packet: IP, expected_packet: IP) -> bool:
317322
if received_packet.src != expected_packet.src or received_packet.dst != expected_packet.dst:
318323
return False
319324
return True
325+
326+
327+
def assess_performance_by_packet(
328+
packet: Packet, duration: float, send_mpps: int | None = None
329+
) -> PerformanceTrafficStats:
330+
"""Send a given packet for a given duration and assess basic performance statistics.
331+
332+
Send `packet` and assess NIC performance for a given duration, corresponding to the test
333+
suite's given topology.
334+
335+
Args:
336+
packet: The packet to send.
337+
duration: Performance test duration (in seconds).
338+
send_mpps: The millions packets per second send rate.
339+
340+
Returns:
341+
Performance statistics of the generated test.
342+
"""
343+
from framework.testbed_model.traffic_generator.performance_traffic_generator import (
344+
PerformanceTrafficGenerator,
345+
)
346+
347+
assert isinstance(
348+
get_ctx().perf_tg, PerformanceTrafficGenerator
349+
), "Cannot send performance traffic with non-performance traffic generator"
350+
tg: PerformanceTrafficGenerator = cast(PerformanceTrafficGenerator, get_ctx().perf_tg)
351+
# TODO: implement @requires for types of traffic generator
352+
return tg.calculate_traffic_and_stats(packet, duration, send_mpps)

dts/api/test.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
This module provides utility functions for test cases, including logging, verification.
77
"""
88

9+
import json
10+
from datetime import datetime
11+
12+
from api.artifact import Artifact
913
from framework.context import get_ctx
1014
from framework.exception import InternalError, SkippedTestException, TestCaseVerifyError
1115
from framework.logger import DTSLogger
@@ -124,3 +128,31 @@ def get_logger() -> DTSLogger:
124128
if current_test_suite is None:
125129
raise InternalError("No current test suite")
126130
return current_test_suite._logger
131+
132+
133+
def write_performance_json(
134+
performance_data: dict, filename: str = "performance_metrics.json"
135+
) -> None:
136+
"""Write performance test results to a JSON file in the test suite's output directory.
137+
138+
This method creates a JSON file containing performance metrics in the test suite's
139+
output directory. The data can be a dictionary of any structure. No specific format
140+
is required.
141+
142+
Args:
143+
performance_data: Dictionary containing performance metrics and results.
144+
filename: Name of the JSON file to create.
145+
146+
Raises:
147+
InternalError: If performance data is not provided.
148+
"""
149+
if not performance_data:
150+
raise InternalError("No performance data to write")
151+
152+
perf_data = {"timestamp": datetime.now().isoformat(), **performance_data}
153+
perf_json_artifact = Artifact("local", filename)
154+
155+
with perf_json_artifact.open("w") as json_file:
156+
json.dump(perf_data, json_file, indent=2)
157+
158+
get_logger().info(f"Performance results written to: {perf_json_artifact.local_path}")

dts/configurations/tests_config.example.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,15 @@
33
# Define the custom test suite configurations
44
hello_world:
55
msg: A custom hello world to you!
6+
# single_core_forward_perf:
7+
# test_parameters: # Add frame size / descriptor count combinations as needed
8+
# - frame_size: 64
9+
# num_descriptors: 512
10+
# expected_mpps: 1.0 # Set millions of packets per second according to your devices expected throughput for this given frame size / descriptor count
11+
# - frame_size: 64
12+
# num_descriptors: 1024
13+
# expected_mpps: 1.0
14+
# - frame_size: 512
15+
# num_descriptors: 1024
16+
# expected_mpps: 1.0
17+
# delta_tolerance: 0.05

dts/framework/remote_session/dpdk.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
RemoteDPDKTarballLocation,
2525
RemoteDPDKTreeLocation,
2626
)
27+
from framework.context import get_ctx
2728
from framework.exception import ConfigurationError, RemoteFileNotFoundError
2829
from framework.logger import DTSLogger, get_dts_logger
2930
from framework.params.eal import EalParams
@@ -259,9 +260,21 @@ def _build_dpdk(self) -> None:
259260
Uses the already configured DPDK build configuration. Assumes that the
260261
`remote_dpdk_tree_path` has already been set on the SUT node.
261262
"""
263+
ctx = get_ctx()
264+
# If the SUT is an ice driver device, make sure to build with 16B descriptors.
265+
if (
266+
ctx.topology.sut_port_ingress
267+
and ctx.topology.sut_port_ingress.config.os_driver == "ice"
268+
):
269+
meson_args = MesonArgs(
270+
default_library="static", libdir="lib", c_args="-DRTE_NET_INTEL_USE_16BYTE_DESC"
271+
)
272+
else:
273+
meson_args = MesonArgs(default_library="static", libdir="lib")
274+
262275
self._session.build_dpdk(
263276
self._env_vars,
264-
MesonArgs(default_library="static", libdir="lib"),
277+
meson_args,
265278
self.remote_dpdk_tree_path,
266279
self.remote_dpdk_build_dir,
267280
)
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright(c) 2025 University of New Hampshire
3+
4+
"""Single core forwarding performance test suite.
5+
6+
This suite measures the amount of packets which can be forwarded by DPDK using a single core.
7+
The testsuites takes in as parameters a set of parameters, each consisting of a frame size,
8+
Tx/Rx descriptor count, and the expected MPPS to be forwarded by the DPDK application. The
9+
test leverages a performance traffic generator to send traffic at two paired TestPMD interfaces
10+
on the SUT system, which forward to one another and then back to the traffic generator's ports.
11+
The aggregate packets forwarded by the two TestPMD ports are compared against the expected MPPS
12+
baseline which is given in the test config, in order to determine the test result.
13+
"""
14+
15+
from scapy.layers.inet import IP
16+
from scapy.layers.l2 import Ether
17+
from scapy.packet import Raw
18+
19+
from api.capabilities import (
20+
LinkTopology,
21+
requires_link_topology,
22+
)
23+
from api.packet import assess_performance_by_packet
24+
from api.test import verify, write_performance_json
25+
from api.testpmd import TestPmd
26+
from api.testpmd.config import RXRingParams, TXRingParams
27+
from framework.params.types import TestPmdParamsDict
28+
from framework.test_suite import BaseConfig, TestSuite, perf_test
29+
30+
31+
class Config(BaseConfig):
32+
"""Performance test metrics."""
33+
34+
test_parameters: list[dict[str, int | float]] = [
35+
{"frame_size": 64, "num_descriptors": 1024, "expected_mpps": 1.00},
36+
{"frame_size": 128, "num_descriptors": 1024, "expected_mpps": 1.00},
37+
{"frame_size": 256, "num_descriptors": 1024, "expected_mpps": 1.00},
38+
{"frame_size": 512, "num_descriptors": 1024, "expected_mpps": 1.00},
39+
{"frame_size": 1024, "num_descriptors": 1024, "expected_mpps": 1.00},
40+
{"frame_size": 1518, "num_descriptors": 1024, "expected_mpps": 1.00},
41+
]
42+
delta_tolerance: float = 0.05
43+
44+
45+
@requires_link_topology(LinkTopology.TWO_LINKS)
46+
class TestSingleCoreForwardPerf(TestSuite):
47+
"""Single core forwarding performance test suite."""
48+
49+
config: Config
50+
51+
def set_up_suite(self):
52+
"""Set up the test suite."""
53+
self.test_parameters = self.config.test_parameters
54+
self.delta_tolerance = self.config.delta_tolerance
55+
56+
def _transmit(self, testpmd: TestPmd, frame_size: int) -> float:
57+
"""Create a testpmd session with every rule in the given list, verify jump behavior.
58+
59+
Args:
60+
testpmd: The testpmd shell to use for forwarding packets.
61+
frame_size: The size of the frame to transmit.
62+
63+
Returns:
64+
The MPPS (millions of packets per second) forwarded by the SUT.
65+
"""
66+
# Build packet with dummy values, and account for the 14B and 20B Ether and IP headers
67+
packet = (
68+
Ether(src="52:00:00:00:00:00")
69+
/ IP(src="1.2.3.4", dst="192.18.1.0")
70+
/ Raw(load="x" * (frame_size - 14 - 20))
71+
)
72+
73+
testpmd.start()
74+
75+
# Transmit for 30 seconds.
76+
stats = assess_performance_by_packet(packet=packet, duration=30)
77+
78+
rx_mpps = stats.rx_pps / 1_000_000
79+
80+
return rx_mpps
81+
82+
def _produce_stats_table(self, test_parameters: list[dict[str, int | float]]) -> None:
83+
"""Display performance results in table format and write to structured JSON file.
84+
85+
Args:
86+
test_parameters: The expected and real stats per set of test parameters.
87+
"""
88+
header = f"{'Frame Size':>12} | {'TXD/RXD':>12} | {'Real MPPS':>12} | {'Expected MPPS':>14}"
89+
print("-" * len(header))
90+
print(header)
91+
print("-" * len(header))
92+
for params in test_parameters:
93+
print(f"{params['frame_size']:>12} | {params['num_descriptors']:>12} | ", end="")
94+
print(f"{params['measured_mpps']:>12.2f} | {params['expected_mpps']:>14.2f}")
95+
print("-" * len(header))
96+
97+
write_performance_json({"results": test_parameters})
98+
99+
@perf_test
100+
def single_core_forward_perf(self) -> None:
101+
"""Validate expected single core forwarding performance.
102+
103+
Steps:
104+
* Create a packet according to the frame size specified in the test config.
105+
* Transmit from the traffic generator's ports 0 and 1 at above the expect.
106+
* Forward on TestPMD's interfaces 0 and 1 with 1 core.
107+
108+
Verify:
109+
* The resulting MPPS forwarded is greater than expected_mpps*(1-delta_tolerance).
110+
"""
111+
# Find SUT DPDK driver to determine driver specific performance optimization flags
112+
sut_dpdk_driver = self._ctx.sut_node.config.ports[0].os_driver_for_dpdk
113+
114+
for params in self.test_parameters:
115+
frame_size = params["frame_size"]
116+
num_descriptors = params["num_descriptors"]
117+
118+
driver_specific_testpmd_args: TestPmdParamsDict = {
119+
"tx_ring": TXRingParams(descriptors=num_descriptors),
120+
"rx_ring": RXRingParams(descriptors=num_descriptors),
121+
"nb_cores": 1,
122+
}
123+
124+
if sut_dpdk_driver == "mlx5_core":
125+
driver_specific_testpmd_args["burst"] = 64
126+
driver_specific_testpmd_args["mbcache"] = 512
127+
elif sut_dpdk_driver == "i40e":
128+
driver_specific_testpmd_args["rx_queues"] = 2
129+
driver_specific_testpmd_args["tx_queues"] = 2
130+
131+
with TestPmd(
132+
**driver_specific_testpmd_args,
133+
) as testpmd:
134+
params["measured_mpps"] = self._transmit(testpmd, frame_size)
135+
params["performance_delta"] = (
136+
float(params["measured_mpps"]) - float(params["expected_mpps"])
137+
) / float(params["expected_mpps"])
138+
params["pass"] = float(params["performance_delta"]) >= -self.delta_tolerance
139+
140+
self._produce_stats_table(self.test_parameters)
141+
142+
for params in self.test_parameters:
143+
verify(
144+
params["pass"] is True,
145+
f"""Packets forwarded is less than {(1 -self.delta_tolerance)*100}%
146+
of the expected baseline.
147+
Measured MPPS = {params["measured_mpps"]}
148+
Expected MPPS = {params["expected_mpps"]}""",
149+
)

0 commit comments

Comments
 (0)