|
| 1 | +# Copyright 2025 PerfKitBenchmarker Authors. All rights reserved. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +"""Runs DPDK Pktgen benchmarks for high-performance networking. |
| 15 | +
|
| 16 | +DPDK Pktgen Benchmark is a more feature-rich DPDK benchmark than dpdk_testpmd. |
| 17 | +DPDK bypasses the kernel networking stack, allowing for much higher PPS. |
| 18 | +
|
| 19 | +Benchmark Documentation: |
| 20 | +https://pktgen-dpdk.readthedocs.io/en/latest/getting_started.html |
| 21 | +https://toonk.io/building-a-high-performance-linux-based-traffic-generator-with-dpdk/index.html |
| 22 | +""" |
| 23 | + |
| 24 | +import re |
| 25 | +from typing import Any, List, Mapping |
| 26 | + |
| 27 | +from absl import flags |
| 28 | +from perfkitbenchmarker import background_tasks |
| 29 | +from perfkitbenchmarker import benchmark_spec as bm_spec |
| 30 | +from perfkitbenchmarker import configs |
| 31 | +from perfkitbenchmarker import linux_virtual_machine |
| 32 | +from perfkitbenchmarker import sample |
| 33 | +from perfkitbenchmarker.linux_packages import dpdk_pktgen |
| 34 | + |
| 35 | + |
| 36 | +BENCHMARK_NAME = 'dpdk_pktgen' |
| 37 | +BENCHMARK_CONFIG = """ |
| 38 | +dpdk_pktgen: |
| 39 | + description: Runs dpdk testpmd benchmarks |
| 40 | + vm_groups: |
| 41 | + vm_1: |
| 42 | + vm_spec: *default_single_core |
| 43 | + vm_2: |
| 44 | + vm_spec: *default_single_core |
| 45 | + flags: |
| 46 | + placement_group_style: closest_supported |
| 47 | + gce_subnet_name: default,dpdk0 |
| 48 | + gce_nic_types: GVNIC,GVNIC |
| 49 | + gce_nic_queue_counts: default,default |
| 50 | + gce_network_type: custom |
| 51 | + ip_addresses: INTERNAL |
| 52 | +""" |
| 53 | + |
| 54 | +FLAGS = flags.FLAGS |
| 55 | + |
| 56 | +_DPDK_PKTGEN_DURATION = flags.DEFINE_integer( |
| 57 | + 'dpdk_pktgen_duration', 60, 'Run duration in seconds.' |
| 58 | +) |
| 59 | +_DPDK_PKTGEN_PACKET_LOSS_THRESHOLDS = flags.DEFINE_multi_float( |
| 60 | + 'dpdk_pktgen_packet_loss_threshold_rates', |
| 61 | + [0, 0.00001, 0.0001], |
| 62 | + 'Packet loss thresholds to record samples for.', |
| 63 | + lower_bound=0, |
| 64 | +) |
| 65 | + |
| 66 | +# DPDK Pktgen maximum logical cores |
| 67 | +_MAX_LCORES = 128 |
| 68 | +# Starting packet transmission rate as a percentage. |
| 69 | +_START_RATE = 0.001 |
| 70 | +# Percent difference in PPS between consecutive iterations to terminate binary |
| 71 | +# search. |
| 72 | +_PPS_BINARY_SEARCH_THRESHOLD = 0.01 |
| 73 | +_STDOUT_LOG_FILE = 'pktgen_stdout.log' |
| 74 | + |
| 75 | + |
| 76 | +def GetConfig(user_config: Mapping[Any, Any]) -> Mapping[Any, Any]: |
| 77 | + """Merge BENCHMARK_CONFIG with user_config to create benchmark_spec. |
| 78 | +
|
| 79 | + Args: |
| 80 | + user_config: user-defined configs (through FLAGS.benchmark_config_file or |
| 81 | + FLAGS.config_override). |
| 82 | +
|
| 83 | + Returns: |
| 84 | + The resulting configs that come from merging user-defined configs with |
| 85 | + BENCHMARK_CONFIG. |
| 86 | + """ |
| 87 | + return configs.LoadConfig(BENCHMARK_CONFIG, user_config, BENCHMARK_NAME) |
| 88 | + |
| 89 | + |
| 90 | +def Prepare(benchmark_spec: bm_spec.BenchmarkSpec) -> None: |
| 91 | + """Prepares both VM's to run DPDK Pktgen. |
| 92 | +
|
| 93 | + Args: |
| 94 | + benchmark_spec: The benchmark specification. |
| 95 | + """ |
| 96 | + sender_vm, receiver_vm = benchmark_spec.vms[:2] |
| 97 | + |
| 98 | + background_tasks.RunThreaded( |
| 99 | + lambda vm: PrepareVM( |
| 100 | + vm, |
| 101 | + sender_vm.internal_ips[1], |
| 102 | + receiver_vm.internal_ips[1], |
| 103 | + ), |
| 104 | + [sender_vm, receiver_vm], |
| 105 | + ) |
| 106 | + receiver_vm.RemoteCommand( |
| 107 | + 'sudo sed -i "s/set all rate <RATE>//g"' |
| 108 | + f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' |
| 109 | + ) |
| 110 | + receiver_vm.RemoteCommand( |
| 111 | + 'sudo sed -i "s/start all//g"' |
| 112 | + f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' |
| 113 | + ) |
| 114 | + # Ensure receiver runs longer than sender. Receiver starts 1 second before |
| 115 | + # sender, so make receiver duration 2 seconds longer. |
| 116 | + receiver_vm.RemoteCommand( |
| 117 | + f'sudo sed -i "s/<DURATION>/{_DPDK_PKTGEN_DURATION.value+2}/g"' |
| 118 | + f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' |
| 119 | + ) |
| 120 | + sender_vm.RemoteCommand( |
| 121 | + f'sudo sed -i "s/<DURATION>/{_DPDK_PKTGEN_DURATION.value}/g"' |
| 122 | + f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' |
| 123 | + ) |
| 124 | + |
| 125 | + |
| 126 | +def PrepareVM( |
| 127 | + vm: linux_virtual_machine.BaseLinuxVirtualMachine, |
| 128 | + sender_vm_ip: str, |
| 129 | + receiver_vm_ip: str, |
| 130 | +) -> None: |
| 131 | + """Prepares a VM to run DPDK Pktgen.""" |
| 132 | + vm.Install('dpdk_pktgen') |
| 133 | + vm.PushDataFile( |
| 134 | + 'dpdk_pktgen/pktgen.pkt', |
| 135 | + f'{dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt', |
| 136 | + ) |
| 137 | + vm.RemoteCommand( |
| 138 | + f'sudo sed -i "s/<SRC_IP>/{sender_vm_ip}/g"' |
| 139 | + f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' |
| 140 | + ) |
| 141 | + vm.RemoteCommand( |
| 142 | + f'sudo sed -i "s/<DST_IP>/{receiver_vm_ip}/g"' |
| 143 | + f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' |
| 144 | + ) |
| 145 | + |
| 146 | + |
| 147 | +def Run(benchmark_spec: bm_spec.BenchmarkSpec) -> list[sample.Sample]: |
| 148 | + """Runs DPDK benchmarks. |
| 149 | +
|
| 150 | + Args: |
| 151 | + benchmark_spec: The benchmark specification. |
| 152 | +
|
| 153 | + Returns: |
| 154 | + A list of sample.Sample objects with the performance results. |
| 155 | +
|
| 156 | + Raises: |
| 157 | + RunError: A run-stage error raised by an individual benchmark. |
| 158 | + """ |
| 159 | + sender_vm, receiver_vm = benchmark_spec.vms[:2] |
| 160 | + samples = [] |
| 161 | + |
| 162 | + num_lcores = min(sender_vm.NumCpusForBenchmark(), _MAX_LCORES) |
| 163 | + num_memory_channels_stdout, _ = sender_vm.RemoteCommand( |
| 164 | + "sudo dmidecode --type memory | grep -c 'Size:'" |
| 165 | + ) |
| 166 | + num_memory_channels = int(num_memory_channels_stdout) |
| 167 | + metadata = { |
| 168 | + 'dpdk_pkgen_burst': 1, |
| 169 | + 'dpdk_pktgen_lcores': num_lcores, |
| 170 | + 'dpdk_pktgen_num_memory_channels': num_memory_channels, |
| 171 | + 'dpdk_pktgen_duration': _DPDK_PKTGEN_DURATION.value, |
| 172 | + } |
| 173 | + cmd = ( |
| 174 | + f'cd {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR} && sudo' |
| 175 | + f' ./usr/local/bin/pktgen -l 0-{num_lcores-1} -n {num_memory_channels} --' |
| 176 | + f' -m "[1-7].0" -f pktgen.pkt > {_STDOUT_LOG_FILE}' |
| 177 | + ) |
| 178 | + |
| 179 | + prev_rate = '<RATE>' |
| 180 | + for packet_loss_threshold in _DPDK_PKTGEN_PACKET_LOSS_THRESHOLDS.value: |
| 181 | + metadata = metadata.copy() |
| 182 | + metadata['dpdk_pktgen_packet_loss_threshold'] = packet_loss_threshold |
| 183 | + valid_total_sender_tx_pkts = None |
| 184 | + valid_total_sender_rx_pkts = None |
| 185 | + valid_total_receiver_rx_pkts = None |
| 186 | + valid_packet_loss_rate = 1 |
| 187 | + # Binary search for max PPS under packet loss rate thresholds. |
| 188 | + prev_pps, curr_pps = -float('inf'), 0 |
| 189 | + lb, ub = 0, _START_RATE * 2 |
| 190 | + |
| 191 | + while ( |
| 192 | + abs(curr_pps - prev_pps) / (curr_pps + 1) |
| 193 | + ) > _PPS_BINARY_SEARCH_THRESHOLD: |
| 194 | + curr_rate = (lb + ub) / 2 |
| 195 | + sender_vm.RemoteCommand( |
| 196 | + f'sudo sed -i "s/{prev_rate}/{curr_rate}/g"' |
| 197 | + f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' |
| 198 | + ) |
| 199 | + |
| 200 | + # Running Pktgen requires a terminal. |
| 201 | + background_tasks.RunThreaded( |
| 202 | + lambda vm: vm.RemoteCommand( |
| 203 | + cmd, login_shell=True, disable_tty_lock=True |
| 204 | + ), |
| 205 | + [receiver_vm, sender_vm], |
| 206 | + post_task_delay=1, # Ensure receiver starts before sender. |
| 207 | + ) |
| 208 | + total_sender_tx_pkts, total_sender_rx_pkts, total_receiver_rx_pkts = ( |
| 209 | + _ParseStdout(sender_vm, receiver_vm) |
| 210 | + ) |
| 211 | + |
| 212 | + packet_loss_rate = ( |
| 213 | + int(total_sender_tx_pkts) |
| 214 | + + int(total_sender_rx_pkts) |
| 215 | + - int(total_receiver_rx_pkts) |
| 216 | + ) / int(total_sender_tx_pkts) |
| 217 | + if packet_loss_rate > packet_loss_threshold: |
| 218 | + ub = curr_rate |
| 219 | + else: |
| 220 | + valid_total_sender_tx_pkts = total_sender_tx_pkts |
| 221 | + valid_total_sender_rx_pkts = total_sender_rx_pkts |
| 222 | + valid_total_receiver_rx_pkts = total_receiver_rx_pkts |
| 223 | + valid_packet_loss_rate = packet_loss_rate |
| 224 | + lb = curr_rate |
| 225 | + prev_pps, curr_pps = ( |
| 226 | + curr_pps, |
| 227 | + int(total_receiver_rx_pkts) // _DPDK_PKTGEN_DURATION.value, |
| 228 | + ) |
| 229 | + prev_rate = curr_rate |
| 230 | + |
| 231 | + samples.extend([ |
| 232 | + sample.Sample( |
| 233 | + 'Total sender tx packets', |
| 234 | + int(valid_total_sender_tx_pkts), |
| 235 | + 'packets', |
| 236 | + metadata, |
| 237 | + ), |
| 238 | + sample.Sample( |
| 239 | + 'Total sender tx pps', |
| 240 | + int(valid_total_sender_tx_pkts) // _DPDK_PKTGEN_DURATION.value, |
| 241 | + 'packets/s', |
| 242 | + metadata, |
| 243 | + ), |
| 244 | + sample.Sample( |
| 245 | + 'Total sender rx packets', |
| 246 | + int(valid_total_sender_rx_pkts), |
| 247 | + 'packets', |
| 248 | + metadata, |
| 249 | + ), |
| 250 | + sample.Sample( |
| 251 | + 'Total sender rx pps', |
| 252 | + int(valid_total_sender_rx_pkts) // _DPDK_PKTGEN_DURATION.value, |
| 253 | + 'packets/s', |
| 254 | + metadata, |
| 255 | + ), |
| 256 | + sample.Sample( |
| 257 | + 'Total receiver rx packets', |
| 258 | + int(valid_total_receiver_rx_pkts), |
| 259 | + 'packets', |
| 260 | + metadata, |
| 261 | + ), |
| 262 | + sample.Sample( |
| 263 | + 'Total receiver rx pps', |
| 264 | + int(valid_total_receiver_rx_pkts) // _DPDK_PKTGEN_DURATION.value, |
| 265 | + 'packets/s', |
| 266 | + metadata, |
| 267 | + ), |
| 268 | + sample.Sample( |
| 269 | + 'packet loss rate', |
| 270 | + valid_packet_loss_rate, |
| 271 | + 'rate (1=100%)', |
| 272 | + metadata, |
| 273 | + ), |
| 274 | + ]) |
| 275 | + |
| 276 | + return samples |
| 277 | + |
| 278 | + |
| 279 | +def _ParseStdout( |
| 280 | + sender_vm: linux_virtual_machine.BaseLinuxVirtualMachine, |
| 281 | + receiver_vm: linux_virtual_machine.BaseLinuxVirtualMachine, |
| 282 | +) -> List[str]: |
| 283 | + """Parse stdout log file to obtain packets sent and received. |
| 284 | +
|
| 285 | + Args: |
| 286 | + sender_vm: The sender VM. |
| 287 | + receiver_vm: The receiver VM. |
| 288 | +
|
| 289 | + Returns: |
| 290 | + A list of strings representing the total sender tx packets, total sender rx |
| 291 | + packets, and total receiver rx packets. |
| 292 | + """ |
| 293 | + # Filter out ANSI escape codes from stdout. |
| 294 | + stdout_parser = ( |
| 295 | + f'cat {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/{_STDOUT_LOG_FILE} |' |
| 296 | + r' sed "s/\x1B\[[0-9;]*[a-zA-Z]//g"' |
| 297 | + ) |
| 298 | + receiver_stdout, _ = receiver_vm.RemoteCommand(stdout_parser) |
| 299 | + sender_stdout, _ = sender_vm.RemoteCommand(stdout_parser) |
| 300 | + total_sender_rx_pkts, total_sender_tx_pkts = re.findall( |
| 301 | + r'Powered by DPDK.*', sender_stdout |
| 302 | + )[-1].split()[30:32] |
| 303 | + total_receiver_rx_pkts = re.findall(r'Powered by DPDK.*', receiver_stdout)[ |
| 304 | + -1 |
| 305 | + ].split()[30] |
| 306 | + |
| 307 | + return [total_sender_tx_pkts, total_sender_rx_pkts, total_receiver_rx_pkts] |
| 308 | + |
| 309 | + |
| 310 | +def Cleanup(benchmark_spec: bm_spec.BenchmarkSpec) -> None: |
| 311 | + """Cleanup benchmarks on the target vm. |
| 312 | +
|
| 313 | + Args: |
| 314 | + benchmark_spec: The benchmark specification. |
| 315 | + """ |
| 316 | + del benchmark_spec |
0 commit comments