Skip to content

feat(dpdk-stdlib-quic): real-NIC QUIC perf path + quic-perf-client - #100

Merged
gspivey merged 2 commits into
developmentfrom
agent/quic-real-nic
Jul 1, 2026
Merged

gspivey merged 2 commits into
developmentfrom
agent/quic-real-nic

Conversation

@gspivey

@gspivey gspivey commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Makes QUIC benchmarkable over a real DPDK NIC (not just in-process loopback) and keeps the existing loopback bench intact.

Stacked on #99 (for DpdkBackend::new_real_nic). GitHub will retarget this PR to development when #99 merges. Merge #99 first.

The bug this fixes

The QUIC provider built its backend via create_backend/DpdkBackend::new, which init EAL with --no-pci — so it never probes the vfio-pci ENI (a null device). Every "native-dpdk" QUIC path (including the existing quic-echo-server/quic-test-client integration tier) was therefore running against a null device, and ProviderConfig.eal_args was dead config that never reached EAL.

Changes

  • provider.rs — start() now builds the backend via DpdkBackend::new_real_nic(port_id, eal_args) for Dpdk/Auto backend types (real PCI probe; honors eal_args). backend_override (loopback bench) still takes precedence, so bench.rs is unaffected. New ProviderBuilder::with_dpdk_port.
  • quic-echo-server — --eal-args/--dpdk-port, and a --throughput mode that stream-echoes each chunk (for long-lived sustained streams). Cert-PEM stdout protocol + QUIC_SERVER_READY marker unchanged.
  • quic-perf-client (new bin) — drives N bidi streams for a fixed duration and prints one machine-parseable PERF_RESULT gbps=.. bytes=.. hs_us=.. rx_bursts=.. tx_bursts=.. rx_drops=.. tx_drops=.. line.

Validation (toolchain 1.91.0, warm cache)

  • cargo build --workspace — clean
  • cargo test -p dpdk-stdlib-quic — 63 passed / 0 failed (45 lib unit incl. new real-backend-path test; 1 perf-client gbps math; loopback handshake/backend/ecn/gso/provider-init integration tests all green → loopback bench unaffected)

Notes / follow-ups

  • v1 measures request/response goodput (one payload in flight per stream), parallelised across --streams. Full pipelining + TX batching/GSO are future work; the backend also allocs/copies one mbuf per datagram, which bounds line rate.
  • The 2-instance orchestration (run-quic-perf.sh) and the run-perf-tests.sh quic-* tokens are intentionally in the next PR (perf-runner wiring), to keep the shared script out of the parallel feature PRs.

🤖 Generated with Claude Code

gspivey and others added 2 commits June 30, 2026 08:58
…ly run

The TCP apps called TcpListener::bind / TcpStream::connect, which require a
process-wide TcpContext + a running engine — but nothing built either, so an
IPv4 bind failed with "TCP context not initialized". This wires up the missing
runtime and fixes two engine bugs the work surfaced.

- dpdk-stdlib-net: add DpdkBackend::new_real_nic(port_id, eal_args) which inits
  EAL WITHOUT --no-pci (mirrors dpdk-stdlib-udp's get_or_init_dpdk), so a real
  vfio-pci ENI is actually probed. DpdkBackend::new (--no-pci) is a null device
  that never sees the NIC — using it for TCP/QUIC was the root cause of "real
  DPDK" paths touching nothing. Serialize the EAL-lifecycle backend tests
  (#[serial]) since the stub's EAL state is process-global.
- dpdk-stdlib-tcp: new runtime module with init_dpdk_tcp_context() — builds the
  backend (new_real_nic) + ArpResolver(gateway_mac) + engine + spawns the single
  engine-driver thread, then publishes the global TcpContext.
- Engine bug 1 (zeroed MACs): the driver now extracts the Ethernet src/dst MAC
  from the raw frame and calls on_segment_with_macs, so a server's SYN-ACK is
  addressed to the real peer (gateway) MAC instead of 00:..:00. Same fix applied
  to stream.rs::engine_loop.
- Engine bug 2 (dead command sender): accept-side ConnectionHandles got a
  throwaway dummy CommandSender, so Close/Shutdown/SetOption from accepted
  streams went nowhere and writes never woke the engine. Add TcpEngine::with_cmd_tx
  and thread the live sender through handle_syn / handle_syn_with_macs.
- apps: tcp-echo, tokio-tcp-echo, tcp-test-client gain --gateway-mac / --eal-args
  (+ server --perf-interval, client --local-ip) and call init_dpdk_tcp_context
  before bind/connect. plain-tcp-echo (kernel) is unchanged.

Tests: cargo build --workspace clean; cargo test --workspace = 1031 passed / 0
failed. New unit tests: drive_once SYN-ACK targets the peer MAC (MAC regression),
with_cmd_tx threads the live sender to the accept handle, new_real_nic smoke test.

Perf-runner token wiring (rust-dpdk-tcp / tokio-dpdk-tcp) is deferred to the
perf-runner PR so the shared run-perf-tests.sh doesn't collide across PRs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire a real DPDK NIC into the QUIC provider and add a sustained-throughput
client so QUIC can be benchmarked over a real ENI, not just in-process loopback.

- provider: start() now builds the backend via DpdkBackend::new_real_nic for
  Dpdk/Auto backend types (vs create_backend/DpdkBackend::new, which init EAL
  with --no-pci and never probe the vfio ENI -- a null device). This also
  finally routes ProviderConfig.eal_args into EAL (previously dead config),
  and makes the existing quic-echo-server/quic-test-client integration path
  use a real NIC. Add ProviderBuilder::with_dpdk_port.
- quic-echo-server: add --eal-args/--dpdk-port and a --throughput mode that
  stream-echoes each chunk (for long-lived sustained streams); keeps the
  cert-PEM stdout protocol and QUIC_SERVER_READY marker intact.
- quic-perf-client (new bin): drives N bidi streams for a fixed duration and
  prints one PERF_RESULT line (gbps, bytes, handshake us, provider stats).
- Loopback bench (bench.rs) is unchanged: it uses the backend_override path,
  which takes precedence over new_real_nic.
- Tests: provider real-backend-path test (+ #[serial] on EAL-touching provider
  tests, matching the net crate), gbps math test. cargo test -p dpdk-stdlib-quic
  green; cargo build --workspace clean.

Stacked on the TCP engine-driver PR for DpdkBackend::new_real_nic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Base automatically changed from agent/tcp-dpdk-engine to development July 1, 2026 09:39
@gspivey
gspivey merged commit 3f3ec9a into development Jul 1, 2026
@gspivey
gspivey deleted the agent/quic-real-nic branch July 1, 2026 09:40
gspivey added a commit that referenced this pull request Jul 1, 2026
…nfigs tokens (no YAML) (#101)

## Summary

Wires **QUIC IPv4**, **TCP IPv4**, and **UDP IPv6** performance tests
into the existing perf runner so they're dispatchable through
`perf-tests.yml` **with zero `.github/workflows` edits** — the workflow
is `workflow_dispatch` with a free-text `configs` input, so new
`--configs` tokens decoded inside `run-perf-tests.sh` are all that's
needed. This deliberately sidesteps the `workflow`-scope push blocker
(the same wall PR #97 is stuck behind).

All new tokens are **purely additive** — existing UDP behavior and the
default `CONFIGS` are unchanged.

## New `--configs` tokens

| Token | Suite | DUT | Benchmark |
|---|---|---|---|
| `rust-dpdk-tcp` | TCP v4 | `tcp-echo` (DPDK) | TRex ASTF |
| `tokio-dpdk-tcp` | TCP v4 | `tokio-tcp-echo` (DPDK) | TRex ASTF |
| `plain-rust-tcp` | TCP v4 | `plain-tcp-echo` (kernel) | TRex ASTF |
| `native-dpdk-v6` | UDP v6 | testpmd 5tswap | TRex STL (v6) |
| `rust-dpdk-v6` | UDP v6 | `echo` (DPDK) | TRex STL (v6) |
| `quic-stock` | QUIC | `quic-bench` loopback (stock) | in-process |
| `quic-native-dpdk` | QUIC | `quic-bench` loopback (native) |
in-process |
| `quic-native-dpdk-nic` | QUIC | `quic-echo-server`/`quic-perf-client`
| real-NIC, 2-instance |

## How it works

- **TCP**: `run_benchmark_for_config` branches on the `-tcp` suffix to
the ASTF (stateful) `run_tcp_benchmark.py`
(`--payload-sizes`/`--cps-rates`) instead of the STL/UDP
`run_benchmark.py`. New `start_dut_*_tcp` launchers reuse
`TREX_GATEWAY_MAC` for the DUT `--gateway-mac` (same VPC subnet → same
router MAC).
- **UDP IPv6**: any `-v6` token auto-enables `IP_VERSION=6` so the
existing Phase 2c assigns v6 addresses; `run_benchmark.py` already
auto-detects v6 from `--dst-ip`. `native-dpdk-v6` reuses testpmd
(IP-agnostic); `rust-dpdk-v6` runs `echo` with
`--gateway-mac`/`--peer-ip` NDP seed.
- **QUIC**: an early shim in `main()` short-circuits before any
PerfTestStack deploy (QUIC is app-to-app — TRex can't speak QUIC) and
delegates to `run-quic-benchmarks.sh` (loopback) / the new
**`scripts/run-quic-perf.sh`** (real-NIC, DpdkTestStack, cert transfer +
`PERF_RESULT` scrape).
- **Reporting**: v6 tokens added to the UDP tables; a new **TCP (ASTF)**
results table; `aggregate_results` made dict-safe for the list-shaped
TCP/QUIC `results`.

## Run it

```bash
gh workflow run perf-tests.yml --ref <branch> -f configs=tcp-native-dpdk     # ⇒ rust-dpdk-tcp etc.
gh workflow run perf-tests.yml --ref <branch> -f configs=rust-dpdk-tcp,tokio-dpdk-tcp,plain-rust-tcp
gh workflow run perf-tests.yml --ref <branch> -f configs=native-dpdk-v6,rust-dpdk-v6
gh workflow run perf-tests.yml --ref <branch> -f configs=quic-stock,quic-native-dpdk,quic-native-dpdk-nic
```

## Validation

- `bash -n` clean on `run-perf-tests.sh` + `run-quic-perf.sh`
- `python3 -m py_compile` clean on `run_benchmark.py` +
`run_tcp_benchmark.py`
- All embedded Python heredocs extracted and compiled OK
- (Real perf runs need AWS — validated by dispatch after merge.)

## Dependencies

Needs **#98** (IPv6/NDP), **#99** (TCP engine driver), **#100**
(real-NIC QUIC) merged for the DUT binaries/args these tokens invoke.
Scripts-only PR — no `.github/workflows` edit, no `workflow`-scope push
required.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant