diff --git a/CMakeLists.txt b/CMakeLists.txt index 55469f8..67395e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ build_lib( utils/format-utils.cc utils/switch-api.cc utils/p4-queue.cc + utils/p4-traffic-manager.cc utils/fattree-topo-helper.cc model/switched-ethernet-channel.cc model/eth-net-device.cc @@ -64,6 +65,7 @@ build_lib( helper/build-flowtable-helper.cc HEADER_FILES # equivalent to headers.source utils/p4-queue.h + utils/p4-traffic-manager.h utils/format-utils.h utils/switch-api.h utils/register-access-v1model.h @@ -89,6 +91,7 @@ build_lib( ${third_party_libs} TEST_SOURCES # equivalent to module_test.source test/p4-switch-queue-item-test-suite.cc + test/p4-traffic-manager-test-suite.cc # test/p4-controller-test-suite.cc # test/p4sim-test-suite.cc # test/format-utils-test-suite.cc diff --git a/doc/traffic-manager.md b/doc/traffic-manager.md new file mode 100644 index 0000000..102b56c --- /dev/null +++ b/doc/traffic-manager.md @@ -0,0 +1,349 @@ +# Traffic Manager: VOQ + fabric + strict-priority egress + +This document describes the high-fidelity switch Traffic Manager (TM) added to +the P4sim V1model core, the switch/channel changes that support it, the +configuration surface, and the examples and tests used to validate it. + +The TM replaces the old output-only priority scheduler (`NSQueueingLogicPriRL`) +with an input-buffered **Virtual Output Queue (VOQ)** stage, a **fabric +scheduler** that matches inputs to outputs, and a per-output **strict-priority +egress** stage that serialises frames onto the wire at line rate. + +The whole path is **opt-in and additive**: with `EnableVoqFabric=false` (the +default) the switch behaves exactly as before. Nothing on the legacy path was +removed. + +--- + +## 1. Concepts + +Three orthogonal ideas are easy to conflate, so we separate them explicitly: + +| Concept | What it controls | Where it lives in the TM | +| --- | --- | --- | +| **VOQ** | *Queue organisation / buffering* — which queue a packet enters and where it waits on the **input** side, indexed per `[inPort][outPort][priority]`. Avoids head-of-line blocking. | `EnqueueToVoq()` + `m_voq` | +| **Fabric scheduling** | *Matching* — when several VOQs want to move, which input is granted to which output this round. | `RunFabricScheduler()` (priority-first maximal matching) | +| **Strict Priority (SP)** | *Scheduling on the output* — when several egress queues of a port hold packets, which priority is served first. | `SelectEgressPriority()` | +| **FIFO** | *Ordering within one queue* — packets leave a single queue in arrival order. | `std::deque` per `[out][prio]` | + +The egress side is therefore **strict priority across the 8 priority queues of a +port, FIFO within each queue** — i.e. per output port it behaves like a `pfifo` +(strict-priority + FIFO) scheduler. Priority is a 3-bit field, so there are 8 +levels; **higher value = higher priority** (7 is highest). + +--- + +## 2. Datapath + +``` + ingress pipeline + │ (outPort, priority chosen by the P4 program) + ▼ + EnqueueToVoq ──► VOQ[inPort][outPort][priority] (input-buffered) + │ + ▼ + fabric scheduler (priority-first + grants in→out maximal matching) + │ + ▼ + egress[outPort][priority] (per-port SP + FIFO) + │ + ▼ + egress pipeline + deparse + │ + ▼ + TransmitCallback ──► ns-3 NetDevice ──► wire + ▲ │ + └──── NotifyEgressTxComplete ◄───────┘ + (frame finished serialising) +``` + +### Fabric policy + +The default matching is **priority-first maximal matching**: for priority 7 down +to 0, for each still-unused input port, grant the first unused output port that +has a non-empty VOQ at that priority. One input grants at most one packet and one +output receives at most one packet per round. The policy is a single overridable +method (`DoRunFabricScheduler()`) so iSLIP / round-robin can be dropped in +without touching the rest of the TM. + +### Event-driven vs manual + +- **Manual (default object mode):** call `RunFabricScheduler()` / + `DequeueFromVoq()` yourself; nothing touches the ns-3 event queue. Used by the + low-level unit tests. +- **Event-driven (`EventDriven=true`):** `EnqueueToVoq()` arms a self-clocking + fabric+egress loop via `Simulator::Schedule` (no threads) that moves packets + VOQ → egress → wire, honouring the fabric/port rates and the + arbitration/pipeline delays. This is the mode the V1model core uses. + +### Completion-driven egress and the channel signal + +By default the egress scheduler self-clocks each port's serialisation at +`PortRate`. With `EgressCompletionDriven=true` (the mode the core uses) the flow +is decoupled instead: + +1. `EgressServiceEvent()` hands the frame to the `TransmitCallback` **first**, + then waits — it does **not** immediately count the frame or re-arm the port. +2. The datapath (the switched-Ethernet channel / PHY) decides when the frame has + finished serialising onto the wire and calls + `NotifyEgressTxComplete(outPort, success)`. +3. Only on that signal does the TM count the frame as transmitted + (`totalTransmitted`, `perPriorityTransmitted`, `perPortTxBytes`), clear the + in-flight slot, and serve the next frame. + +This separates *"which packet goes next"* (the TM's decision) from *"when the +wire is free"* (the PHY's), and guarantees `totalTransmitted` counts a frame only +**after** it is actually on the wire — never before. + +The accounting is split accordingly: **queue-residence** work (buffer release, +egress/total delay, egress-dequeue trace) happens at dequeue; **on-wire** +counters happen on completion. + +### Switched-Ethernet channel: propagation no longer blocks the next frame + +`SwitchedEthernetChannel` models a full-duplex link with independent per-source +state. The sender is freed after **serialisation** (`txTime`), not after +**propagation** (`txTime + delay`): once a frame's last bit has left the port the +port returns to `IDLE` and may start the next frame, while the in-flight frame +keeps propagating toward the receiver. `GetState()` still reports `PROPAGATING` +for observers, and `IsBusy()` (which gates `TransmitStart`) is true only while a +frame is actively serialising. This keeps back-to-back frames flowing at line +rate instead of paying the propagation delay per frame. + +--- + +## 3. Enabling the TM + +Set one attribute on the P4 switch device before the simulation starts: + +```cpp +P4Helper p4; +p4.SetDeviceAttribute("JsonPath", StringValue(jsonPath)); +p4.SetDeviceAttribute("FlowTablePath", StringValue(flowTablePath)); +p4.SetDeviceAttribute("P4SwitchArch", UintegerValue(0)); // V1model +p4.SetDeviceAttribute("EnableVoqFabric", BooleanValue(true)); // ← opt in +``` + +The core then creates the TM at start-up, sizes it to the number of attached +ports, seeds `PortRate` from the egress channel's `DataRate`, wires the +`TransmitCallback` to the egress send path, and enables `EventDriven` + +`EgressCompletionDriven`. The TM is disposed before the core is destroyed. + +--- + +## 4. Configuration surface + +All knobs are ns-3 attributes on `ns3::P4TrafficManager` (buffer limits are in +**bytes**; `0` means unlimited): + +| Attribute | Type | Default | Meaning | +| --- | --- | --- | --- | +| `NumPorts` | uint32 | 0 | Number of ports N; allocates N·N·8 VOQs. Set by the core. | +| `GlobalBufferLimit` | uint64 | 0 | Total bytes across VOQ + egress. | +| `InputBufferLimit` | uint64 | 0 | Per-input-port bytes. | +| `VoqLimit` | uint64 | 0 | Per-VOQ `[in][out][prio]` bytes. | +| `EgressPortLimit` | uint64 | 0 | Per-output-port egress bytes. | +| `EgressQueueLimit` | uint64 | 0 | Per-egress-queue `[out][prio]` bytes. | +| `PortRate` | DataRate | 1Gbps | Output-port serialisation rate. | +| `FabricRate` | DataRate | 10Gbps | Fabric transfer rate. | +| `IngressPipelineDelay` | Time | 0 | Fixed ingress processing delay (applied before the fabric round). | +| `FabricArbitrationDelay` | Time | 0 | Fixed fabric arbitration delay per round. | +| `EgressPipelineDelay` | Time | 0 | Fixed egress processing delay. | +| `EventDriven` | bool | false | Self-clock the fabric+egress loop via ns-3 events. | +| `EgressCompletionDriven` | bool | false | Wait for `NotifyEgressTxComplete()` before counting a frame transmitted (requires `EventDriven`). | + +Switch-level knob (on `ns3::P4SwitchNetDevice`): + +| Attribute | Type | Default | Meaning | +| --- | --- | --- | --- | +| `EnableVoqFabric` | bool | false | Route egress through the VOQ+fabric TM instead of the legacy output-queued path. | + +--- + +## 5. Statistics, traces, and drop reasons + +`GetStats()` returns a cumulative `TmStats`: + +| Counter | Meaning | +| --- | --- | +| `totalReceived` | packets offered to `EnqueueToVoq` | +| `totalVoqEnqueued` | accepted into a VOQ | +| `totalMovedToEgress` | dequeued from VOQ (granted by the fabric) | +| `totalEgressEnqueued` | accepted into an egress queue | +| `totalTransmitted` | serialised onto the wire (counted **after** transmit) | +| `totalDropped` | dropped for any reason | +| `dropsByReason[5]` | per-`TmDropReason` breakdown | +| `perPriorityTransmitted[8]` | transmitted per priority level | +| `perPortTxBytes[]` | bytes transmitted per output port | +| `AvgVoqDelay/AvgEgressDelay/AvgTotalDelay`, `maxQueueingDelay` | delay accumulators | + +Trace sources: `VoqEnqueue`, `VoqDequeue`, `EgressEnqueue`, `EgressDequeue`, +`Drop`, `VoqWaitingDelay`, `EgressWaitingDelay`, `TotalDelay`. + +Drop reasons (`TmDropReason`), checked in admission order: + +1. `VOQ_GLOBAL_BUFFER_FULL` — global buffer would overflow +2. `VOQ_INPUT_BUFFER_FULL` — per-input-port buffer would overflow +3. `VOQ_QUEUE_FULL` — the target `VOQ[in][out][prio]` would overflow +4. `EGRESS_PORT_BUFFER_FULL` — per-output-port egress buffer would overflow +5. `EGRESS_QUEUE_FULL` — the target egress queue would overflow + +--- + +## 6. Validation + +Three levels of validation were run: a unit test suite (TM logic in isolation), +an end-to-end integration example (real P4 program through the switch), and a +throughput benchmark (goodput vs line rate). All results below are from the +current branch. + +### 6.1 Unit test suite — `test/p4-traffic-manager-test-suite.cc` + +9 QUICK cases covering enqueue/dequeue, priority scheduling, VOQ isolation for a +shared output, fabric matching, finite-buffer drops, delay measurement, +event-driven drain, egress strict priority, and egress drop: + +``` +$ ./test.py -s p4-traffic-manager +[1/1] PASS: TestSuite p4-traffic-manager +1 of 1 tests passed (1 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors) +``` + +Cases: `TmBasicEnqueueDequeueTest`, `TmPrioritySchedulingTest`, +`TmVoqSameOutputTest`, `TmFabricMatchingTest`, `TmBufferDropTest`, +`TmDelayMeasurementTest`, `TmEventDrivenDrainTest`, `TmEgressStrictPriorityTest`, +`TmEgressDropTest`. + +### 6.2 Integration example — `examples/p4-voq-fabric-integration.cc` + +Two hosts and one V1model switch running the `simple_v1model` IPv4-forwarding +program. The example is self-validating (non-zero exit on failure) and runs both +datapaths so the additive contract is checked directly. + +``` +$ ./ns3 run "p4-voq-fabric-integration --run=voq" + rxBytes=296000 tmPresent=1 tmReceived=298 tmVoqEnqueued=298 tmTransmitted=298 tmDropped=0 + [PASS] V1model core exists + [PASS] Traffic Manager created when EnableVoqFabric=true + [PASS] Sink received data over the VOQ datapath + [PASS] Packets entered a VOQ + [PASS] TM serialised packets onto the wire + [PASS] Transmitted count does not exceed VOQ-enqueued count + [PASS] VOQ-enqueued count does not exceed offered count +=== ALL CHECKS PASSED (0 failure(s)) === + +$ ./ns3 run "p4-voq-fabric-integration --run=legacy" + rxBytes=296000 tmPresent=0 + [PASS] V1model core exists + [PASS] No Traffic Manager created when disabled (additive contract) + [PASS] Sink received data over the legacy datapath +=== ALL CHECKS PASSED (0 failure(s)) === +``` + +Both datapaths deliver the same 296 000 bytes; the VOQ path additionally shows +`received == voqEnqueued == transmitted` with zero drops (nothing lost inside the +TM), and the legacy path confirms no TM is created when the feature is off. + +### 6.3 Throughput benchmark — `examples/p4-voq-fabric-throughput.cc` + +A single saturating UDP flow (offered at 1.2× the egress line rate) is pushed +host0 → switch → host1. The topology uses a fast ingress link (10 Gbps) so the +sender NIC is never the limiter, and the switch egress port is the sole +bottleneck. Goodput at the sink is compared against the link's line rate; the +header-overhead ceiling is `payload / (payload + 14 + 20 + 8)` ≈ 97.09 % for a +1400-byte payload. + +``` +$ ./ns3 run "p4-voq-fabric-throughput --linkRate=100Mbps" + [TM] received=2059 voqEnq=2059 transmitted=2059 dropped=0 + goodput=97.13 Mbps of 100.00 Mbps line (97.13% of line; header-overhead ceiling ~97.09%) + [PASS] goodput >= 80.00% of line rate + +$ ./ns3 run "p4-voq-fabric-throughput --linkRate=1000Mbps" + [TM] received=20574 voqEnq=20574 transmitted=20574 dropped=0 + goodput=970.92 Mbps of 1000.00 Mbps line (97.09% of line; header-overhead ceiling ~97.09%) + [PASS] goodput >= 80.00% of line rate +``` + +| Egress line rate | Goodput | % of line | Header-overhead ceiling | Drops | +| --- | --- | --- | --- | --- | +| 100 Mbps | 97.13 Mbps | 97.13 % | ~97.09 % | 0 | +| 1000 Mbps | 970.92 Mbps | 97.09 % | ~97.09 % | 0 | + +The delivered goodput sits right at the header-overhead ceiling at both rates, +confirming the completion-driven egress serialises at true line rate with no +artificial timer bottleneck and no internal loss. + +### 6.4 Strict-priority demo — `examples/p4-voq-fabric-priority.cc` + +Two saturating UDP flows from two **separate sender hosts** converge on one +receiver through a switch running the `qos` P4 program, which classifies by UDP +destination port (`dport 4000 → priority 3` HIGH, `dport 2000 → priority 1` +LOW). Each flow enters on its own ingress port — so each has its own host NIC +and its own VOQ — and they contend only inside the switch, at the shared +oversubscribed output port. A finite egress buffer turns the excess into +Traffic-Manager drops rather than unbounded queueing. + +``` +$ ./ns3 run "p4-voq-fabric-priority" + egressLink=100Mbps perFlow=0.7x egress line (combined 1.4x) + HIGH=dport 4000 (prio 3) from host0 LOW=dport 2000 (prio 1) from host1 + HIGH: rx=1678600 B ~67.14 Mbps (offered ~70.00 Mbps, retained 95.92%) + LOW : rx=739200 B ~29.57 Mbps (offered ~70.00 Mbps) + [TM] received=2421 transmitted prio3=1199 prio1=528 dropped=690 + [PASS] Both priority classes carried some traffic + [PASS] HIGH priority delivered more than LOW under congestion + [PASS] TM transmitted more prio-3 frames than prio-1 frames + [PASS] HIGH priority protected (retained offered load) + [PASS] TM dropped the excess low-priority load (port oversubscribed) +=== STRICT PRIORITY OBSERVED (0 failure(s)) === +``` + +| Class | Priority | Offered | Delivered | Result | +| --- | --- | --- | --- | --- | +| HIGH | 3 | ~70 Mbps | ~67.1 Mbps (95.9 %) | protected — served in full | +| LOW | 1 | ~70 Mbps | ~29.6 Mbps | throttled to leftover (~line − HIGH) | + +With the port oversubscribed at 1.4×, the HIGH class keeps essentially all of its +offered load while the LOW class is squeezed to the ~30 Mbps the link has left +after HIGH is served, and 690 excess low-priority frames are dropped — exactly +the strict-priority contract. + +--- + +## 7. How to run + +From the ns-3 root (with this module in `contrib/p4sim`): + +```bash +# Unit tests +./test.py -s p4-traffic-manager + +# End-to-end integration check (both datapaths) +./ns3 run "p4-voq-fabric-integration --run=voq" +./ns3 run "p4-voq-fabric-integration --run=legacy" + +# Throughput benchmark (one link rate per invocation — bmv2 cannot be +# re-initialised within a single process) +./ns3 run "p4-voq-fabric-throughput --linkRate=100Mbps" +./ns3 run "p4-voq-fabric-throughput --linkRate=1000Mbps" + +# Strict-priority demo (HIGH vs LOW flow on a congested output) +./ns3 run "p4-voq-fabric-priority" +``` + +--- + +## 8. Source map + +| File | Role | +| --- | --- | +| `utils/p4-traffic-manager.{h,cc}` | VOQ, fabric scheduler, egress SP, stats/traces, completion signal | +| `model/p4-core-v1model.{h,cc}` | Creates/wires/disposes the TM; opt-in egress branch; transmit + completion glue | +| `model/p4-switch-net-device.{h,cc}` | `EnableVoqFabric` attribute; propagates it to the core | +| `model/switched-ethernet-channel.{h,cc}` | Full-duplex link; sender freed after serialisation, not propagation | +| `test/p4-traffic-manager-test-suite.cc` | 9-case unit suite | +| `examples/p4-voq-fabric-integration.cc` | End-to-end additive-contract check | +| `examples/p4-voq-fabric-throughput.cc` | Near-line-rate goodput benchmark | +| `examples/p4-voq-fabric-priority.cc` | Strict-priority demo (HIGH protected, LOW throttled) | diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 30ab23b..b68423a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -32,6 +32,27 @@ build_lib_example( LIBRARIES_TO_LINK ${P4SIM_CSMA_LIBS} ) +# 2 hosts, 1 switch — end-to-end integration check for the VOQ + fabric datapath +build_lib_example( + NAME p4-voq-fabric-integration + SOURCE_FILES p4-voq-fabric-integration.cc + LIBRARIES_TO_LINK ${P4SIM_CSMA_LIBS} +) + +# 2 hosts, 1 switch — throughput benchmark for the VOQ + fabric datapath +build_lib_example( + NAME p4-voq-fabric-throughput + SOURCE_FILES p4-voq-fabric-throughput.cc + LIBRARIES_TO_LINK ${P4SIM_CSMA_LIBS} +) + +# 2 hosts, 1 switch — strict-priority demo (HIGH vs LOW flow) over the VOQ path +build_lib_example( + NAME p4-voq-fabric-priority + SOURCE_FILES p4-voq-fabric-priority.cc + LIBRARIES_TO_LINK ${P4SIM_CSMA_LIBS} +) + # # 3 hosts, 3 routers (line topology) — L3 forwarding # build_lib_example( # NAME p4-l3-router diff --git a/examples/p4-voq-fabric-integration.cc b/examples/p4-voq-fabric-integration.cc new file mode 100644 index 0000000..22d5cc5 --- /dev/null +++ b/examples/p4-voq-fabric-integration.cc @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2025 TU Dresden + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation; + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Authors: Vineet Goel + */ + +/** + * End-to-end integration check for the opt-in VOQ + fabric Traffic Manager + * datapath in the V1model switch core. + * + * A full bmv2 P4 program (simple_v1model IPv4 forwarding) cannot be booted + * inside the ns-3 unit-test runner (bmv2's per-context PHV pools crash there), + * so this end-to-end check ships as a self-validating example instead, matching + * how every other P4-program scenario in this module is exercised. + * + * Topology (mirrors p4-v1model-ipv4-forwarding): + * + * host0 ──[SwitchedEthernetChannel port 0]──┐ + * ├── P4SwitchNetDevice (switch) + * host1 ──[SwitchedEthernetChannel port 1]──┘ + * + * The same UDP flow (host0 -> host1) is run twice: + * 1. legacy output-queued datapath (EnableVoqFabric = false, the default); + * 2. VOQ + fabric datapath (EnableVoqFabric = true). + * + * The program asserts: + * - the VOQ run instantiates a Traffic Manager and actually moves traffic + * through it (VOQ enqueue + wire serialisation counters are non-zero); + * - the legacy run instantiates NO Traffic Manager (additive contract); + * - both datapaths deliver the same offered load (functional parity). + * + * Exit code 0 = all checks passed; non-zero = a check failed. + */ + +#include "ns3/applications-module.h" +#include "ns3/core-module.h" +#include "ns3/format-utils.h" +#include "ns3/internet-module.h" +#include "ns3/network-module.h" +#include "ns3/p4-core-v1model.h" +#include "ns3/p4-helper.h" +#include "ns3/p4-switch-net-device.h" +#include "ns3/p4-traffic-manager.h" +#include "ns3/packet-sink.h" +#include "ns3/switched-ethernet-helper.h" + +#include +#include +#include +#include + +using namespace ns3; + +NS_LOG_COMPONENT_DEFINE("P4VoqFabricIntegration"); + +namespace +{ + +/// Outcome of one simulation run, captured before Simulator::Destroy(). +struct ScenarioResult +{ + uint64_t rxBytes{0}; ///< bytes received at the UDP sink + bool corePresent{false}; ///< V1model core was created + bool tmPresent{false}; ///< Traffic Manager was created (VOQ path active) + uint64_t tmReceived{0}; ///< packets offered to the TM (EnqueueToVoq) + uint64_t tmVoqEnqueued{0}; ///< packets accepted into a VOQ + uint64_t tmTransmitted{0}; ///< packets the TM serialised onto the wire + uint64_t tmDropped{0}; ///< packets the TM dropped +}; + +/** + * Build a 2-host / 1-switch topology running simple_v1model IPv4 forwarding, + * run a short UDP flow host0 -> host1, and capture the results. + * + * \param enableVoq value of the switch's EnableVoqFabric attribute. + * \return captured results (read before Simulator::Destroy()). + */ +ScenarioResult +RunScenario(bool enableVoq) +{ + ScenarioResult r; + + NodeContainer terminals; + terminals.Create(2); + Ptr switchNode = CreateObject(); + + InternetStackHelper internet; + internet.Install(terminals); + internet.Install(switchNode); + + Ipv4AddressHelper ipv4Addr; + ipv4Addr.SetBase("10.1.1.0", "255.255.255.0"); + + const std::string p4Dir = GetP4ExamplePath() + "/simple_v1model"; + + P4Helper p4; + p4.SetDeviceAttribute("JsonPath", StringValue(p4Dir + "/simple_v1model.json")); + p4.SetDeviceAttribute("FlowTablePath", StringValue(p4Dir + "/flowtable_0.txt")); + p4.SetDeviceAttribute("P4SwitchArch", UintegerValue(0)); // V1model + p4.SetDeviceAttribute("SwitchRate", UintegerValue(10000)); + p4.SetDeviceAttribute("EnableVoqFabric", BooleanValue(enableVoq)); + Ptr sw = DynamicCast(p4.Install(switchNode).Get(0)); + + SwitchedEthernetHelper eth; + eth.SetChannelAttribute("DataRate", StringValue("1000Mbps")); + eth.SetChannelAttribute("Delay", StringValue("0.01ms")); + NetDeviceContainer hostDevs = eth.Install(sw, terminals); + + for (uint32_t i = 0; i < terminals.GetN(); ++i) + { + std::ostringstream macStr; + macStr << "00:00:00:00:00:" << std::hex << std::setfill('0') << std::setw(2) << (i + 1); + hostDevs.Get(i)->SetAddress(Mac48Address(macStr.str().c_str())); + ipv4Addr.Assign(hostDevs.Get(i)); + } + + // --- Applications: UDP OnOff (host0) -> PacketSink (host1) --- + const uint16_t serverPort = 9093; + Ptr serverNode = terminals.Get(1); + Ipv4Address serverAddr = serverNode->GetObject()->GetAddress(1, 0).GetLocal(); + + PacketSinkHelper sink("ns3::UdpSocketFactory", + InetSocketAddress(Ipv4Address::GetAny(), serverPort)); + ApplicationContainer sinkApp = sink.Install(serverNode); + sinkApp.Start(Seconds(1.0)); + sinkApp.Stop(Seconds(4.0)); + + OnOffHelper onOff("ns3::UdpSocketFactory", InetSocketAddress(serverAddr, serverPort)); + onOff.SetAttribute("PacketSize", UintegerValue(1000)); + onOff.SetAttribute("DataRate", StringValue("3Mbps")); + onOff.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1]")); + onOff.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0]")); + ApplicationContainer clientApp = onOff.Install(terminals.Get(0)); + clientApp.Start(Seconds(2.0)); + clientApp.Stop(Seconds(2.8)); + + Simulator::Stop(Seconds(4.0)); + Simulator::Run(); + + // --- Capture results while the core / TM still exist --- + r.rxBytes = DynamicCast(sinkApp.Get(0))->GetTotalRx(); + + P4CoreV1model* core = sw->GetV1ModelCore(); + r.corePresent = (core != nullptr); + Ptr tm = core ? core->GetTrafficManager() : nullptr; + r.tmPresent = (tm != nullptr); + if (tm) + { + const auto& s = tm->GetStats(); + r.tmReceived = s.totalReceived; + r.tmVoqEnqueued = s.totalVoqEnqueued; + r.tmTransmitted = s.totalTransmitted; + r.tmDropped = s.totalDropped; + } + + Simulator::Destroy(); + return r; +} + +int g_failures = 0; + +void +Check(bool cond, const std::string& what) +{ + std::cout << " [" << (cond ? "PASS" : "FAIL") << "] " << what << "\n"; + if (!cond) + { + ++g_failures; + } +} + +} // namespace + +int +main(int argc, char* argv[]) +{ + // NOTE: bmv2 cannot be re-initialised in the same process, so this program + // exercises ONE datapath per invocation (selected by --run). Functional + // parity is checked by running it once with --run=legacy and once with + // --run=voq and comparing the reported rxBytes. + std::string run = "voq"; + CommandLine cmd; + cmd.AddValue("run", "Which datapath to exercise: 'voq' (default) or 'legacy'", run); + cmd.Parse(argc, argv); + + std::cout << "=== VOQ + fabric integration check (run=" << run << ") ===\n"; + + if (run == "legacy") + { + std::cout << "-- Legacy output-queued datapath (EnableVoqFabric=false) --\n"; + ScenarioResult legacy = RunScenario(false); + std::cout << " rxBytes=" << legacy.rxBytes << " tmPresent=" << legacy.tmPresent << "\n"; + Check(legacy.corePresent, "V1model core exists"); + Check(!legacy.tmPresent, "No Traffic Manager created when disabled (additive contract)"); + Check(legacy.rxBytes > 0, "Sink received data over the legacy datapath"); + } + else // "voq" (default) -> exercise the VOQ + fabric datapath + { + std::cout << "-- VOQ + fabric datapath (EnableVoqFabric=true) --\n"; + ScenarioResult voq = RunScenario(true); + std::cout << " rxBytes=" << voq.rxBytes << " tmPresent=" << voq.tmPresent + << " tmReceived=" << voq.tmReceived << " tmVoqEnqueued=" << voq.tmVoqEnqueued + << " tmTransmitted=" << voq.tmTransmitted << " tmDropped=" << voq.tmDropped + << "\n"; + Check(voq.corePresent, "V1model core exists"); + Check(voq.tmPresent, "Traffic Manager created when EnableVoqFabric=true"); + Check(voq.rxBytes > 0, "Sink received data over the VOQ datapath"); + Check(voq.tmVoqEnqueued > 0, "Packets entered a VOQ"); + Check(voq.tmTransmitted > 0, "TM serialised packets onto the wire"); + Check(voq.tmTransmitted <= voq.tmVoqEnqueued, + "Transmitted count does not exceed VOQ-enqueued count"); + Check(voq.tmVoqEnqueued <= voq.tmReceived, + "VOQ-enqueued count does not exceed offered count"); + } + + std::cout << "=== " << (g_failures == 0 ? "ALL CHECKS PASSED" : "CHECKS FAILED") << " (" + << g_failures << " failure(s)) ===\n"; + return g_failures == 0 ? 0 : 1; +} diff --git a/examples/p4-voq-fabric-priority.cc b/examples/p4-voq-fabric-priority.cc new file mode 100644 index 0000000..d7fa3ec --- /dev/null +++ b/examples/p4-voq-fabric-priority.cc @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2025 TU Dresden + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation; + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Authors: Vineet Goel + */ + +/** + * Strict-priority demonstration for the VOQ + fabric Traffic Manager. + * + * Two saturating UDP flows from two different sender hosts converge on one + * receiver through a V1model switch running the `qos` P4 program, which + * classifies packets by UDP destination port and writes + * standard_metadata.priority (aliased to intrinsic_metadata.priority, which the + * Traffic Manager reads): + * + * dport 4000 -> priority 3 (HIGH) + * dport 2000 -> priority 1 (LOW) + * + * hostH (10.1.1.1, port 0) ── HIGH (prio 3) ─┐ + * ├─► switch ─►[bottleneck]─► hostR + * hostL (10.1.1.2, port 1) ── LOW (prio 1) ─┘ (10.1.1.3, port 2) + * + * The HIGH and LOW flows enter on *separate* ingress ports, so each has its own + * host NIC and its own Virtual Output Queue (VOQ[in][out=2][prio]); they only + * contend inside the switch, at the shared output port 2. That output link is + * the bottleneck and each flow is offered at 0.7x its line rate, so their + * combined 1.4x oversubscribes it. + * + * The switch egress runs strict priority across the 8 per-port queues and the + * fabric grants priority-first, so the HIGH class is protected (served in full) + * while the LOW class is throttled to the leftover capacity. A finite egress + * buffer (set via P4TrafficManager attribute defaults) makes the excess + * low-priority load visible as Traffic-Manager drops instead of unbounded + * queueing. + * + * ./ns3 run p4-voq-fabric-priority + * + * Exit code 0 = strict priority observed (HIGH protected, HIGH > LOW, excess + * dropped); non-zero = a check failed. + */ + +#include "ns3/applications-module.h" +#include "ns3/core-module.h" +#include "ns3/data-rate.h" +#include "ns3/format-utils.h" +#include "ns3/internet-module.h" +#include "ns3/network-module.h" +#include "ns3/p4-core-v1model.h" +#include "ns3/p4-helper.h" +#include "ns3/p4-switch-net-device.h" +#include "ns3/p4-traffic-manager.h" +#include "ns3/packet-sink.h" +#include "ns3/switched-ethernet-channel.h" +#include "ns3/switched-ethernet-helper.h" + +#include +#include +#include +#include + +using namespace ns3; + +NS_LOG_COMPONENT_DEFINE("P4VoqFabricPriority"); + +namespace +{ + +int g_failures = 0; + +void +Check(bool cond, const std::string& what) +{ + std::cout << " [" << (cond ? "PASS" : "FAIL") << "] " << what << "\n"; + if (!cond) + { + ++g_failures; + } +} + +} // namespace + +int +main(int argc, char* argv[]) +{ + std::string egressLink = "100Mbps"; // bottleneck: switch(port 2) -> receiver + std::string ingressLink = "10Gbps"; // kept fast: senders -> switch + uint32_t pktSize = 1400; // UDP payload bytes + double perFlowFactor = 0.7; // each flow offered = factor * egress line + double flowDuration = 0.2; // seconds of saturating traffic + uint32_t egressBufferBytes = 65536; // finite per-output egress buffer + double protectThreshold = 0.85; // HIGH must retain >= this fraction of offered + + CommandLine cmd; + cmd.AddValue("egressLink", "Bottleneck egress link rate switch->receiver", egressLink); + cmd.AddValue("ingressLink", "Ingress link rate senders->switch (kept fast)", ingressLink); + cmd.AddValue("pktSize", "UDP payload size in bytes", pktSize); + cmd.AddValue("perFlowFactor", "Per-flow offered load as a multiple of the egress line", perFlowFactor); + cmd.AddValue("flowDuration", "Duration of the saturating flows (s)", flowDuration); + cmd.AddValue("egressBufferBytes", "Per-output egress buffer limit in bytes (0 = unlimited)", egressBufferBytes); + cmd.AddValue("protectThreshold", "HIGH must retain >= this fraction of its offered load", protectThreshold); + cmd.Parse(argc, argv); + + const uint64_t egressBps = DataRate(egressLink).GetBitRate(); + const uint64_t perFlowBps = static_cast(egressBps * perFlowFactor); + std::ostringstream perFlowRate; + perFlowRate << perFlowBps << "bps"; + + std::cout << "=== VOQ+fabric strict-priority demo ===\n" + << " egressLink=" << egressLink << " ingressLink=" << ingressLink + << " pktSize=" << pktSize << " perFlow=" << perFlowFactor + << "x egress line (combined " << (2 * perFlowFactor) << "x)\n" + << " HIGH=dport 4000 (prio 3) from host0 LOW=dport 2000 (prio 1) from host1\n" + << " egressBuffer=" << egressBufferBytes << " B\n"; + + // A finite egress buffer turns the oversubscribed low-priority backlog into + // Traffic-Manager drops. The core creates the TM with CreateObject, so the + // attribute default set here is picked up (the core only overrides port + // count, rate, and the event-driven flags — not the buffer limits). + if (egressBufferBytes > 0) + { + Config::SetDefault("ns3::P4TrafficManager::EgressPortLimit", + UintegerValue(egressBufferBytes)); + } + + // ---- Topology: host0 (HIGH), host1 (LOW) -> switch -> host2 (receiver) ---- + NodeContainer terminals; + terminals.Create(3); // 0 = HIGH sender, 1 = LOW sender, 2 = receiver + Ptr switchNode = CreateObject(); + + InternetStackHelper internet; + internet.Install(terminals); + internet.Install(switchNode); + + Ipv4AddressHelper ipv4Addr; + ipv4Addr.SetBase("10.1.1.0", "255.255.255.0"); + + const std::string p4Dir = GetP4ExamplePath() + "/qos"; + + P4Helper p4; + p4.SetDeviceAttribute("JsonPath", StringValue(p4Dir + "/qos.json")); + p4.SetDeviceAttribute("FlowTablePath", StringValue(p4Dir + "/flowtable_priority.txt")); + p4.SetDeviceAttribute("P4SwitchArch", UintegerValue(0)); // V1model + p4.SetDeviceAttribute("SwitchRate", UintegerValue(10000)); + p4.SetDeviceAttribute("EnableVoqFabric", BooleanValue(true)); + Ptr sw = DynamicCast(p4.Install(switchNode).Get(0)); + + SwitchedEthernetHelper eth; + eth.SetChannelAttribute("DataRate", StringValue(ingressLink)); + eth.SetChannelAttribute("Delay", StringValue("1us")); + NetDeviceContainer hostDevs = eth.Install(sw, terminals); + + // The qos flowtable rewrites the destination MAC on forwarding: + // 10.1.1.1 -> port 0, MAC ...:01 (host0, HIGH sender) + // 10.1.1.2 -> port 1, MAC ...:03 (host1, LOW sender) + // 10.1.1.3 -> port 2, MAC ...:05 (host2, receiver) + hostDevs.Get(0)->SetAddress(Mac48Address("00:00:00:00:00:01")); + hostDevs.Get(1)->SetAddress(Mac48Address("00:00:00:00:00:03")); + hostDevs.Get(2)->SetAddress(Mac48Address("00:00:00:00:00:05")); + ipv4Addr.Assign(hostDevs.Get(0)); + ipv4Addr.Assign(hostDevs.Get(1)); + ipv4Addr.Assign(hostDevs.Get(2)); + + // Slow down only the egress link switch(port 2) -> receiver so it is the + // sole bottleneck; the ingress links keep the fast ingressLink rate. + Ptr egressCh = sw->GetPortChannel(2); + egressCh->SetAttribute("DataRate", DataRateValue(DataRate(egressLink))); + + // ---- Two competing UDP flows -> receiver (host2) ---- + const uint16_t highPort = 4000; // qos: prio 3 (HIGH) + const uint16_t lowPort = 2000; // qos: prio 1 (LOW) + Ipv4Address rxAddr = terminals.Get(2)->GetObject()->GetAddress(1, 0).GetLocal(); + + PacketSinkHelper highSink("ns3::UdpSocketFactory", + InetSocketAddress(Ipv4Address::GetAny(), highPort)); + PacketSinkHelper lowSink("ns3::UdpSocketFactory", + InetSocketAddress(Ipv4Address::GetAny(), lowPort)); + ApplicationContainer highSinkApp = highSink.Install(terminals.Get(2)); + ApplicationContainer lowSinkApp = lowSink.Install(terminals.Get(2)); + highSinkApp.Start(Seconds(1.0)); + lowSinkApp.Start(Seconds(1.0)); + highSinkApp.Stop(Seconds(2.0 + flowDuration + 1.0)); + lowSinkApp.Stop(Seconds(2.0 + flowDuration + 1.0)); + + auto makeFlow = [&](uint32_t senderIdx, uint16_t dport) { + OnOffHelper onOff("ns3::UdpSocketFactory", InetSocketAddress(rxAddr, dport)); + onOff.SetAttribute("PacketSize", UintegerValue(pktSize)); + onOff.SetAttribute("DataRate", StringValue(perFlowRate.str())); + onOff.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1]")); + onOff.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0]")); + ApplicationContainer app = onOff.Install(terminals.Get(senderIdx)); + app.Start(Seconds(2.0)); + app.Stop(Seconds(2.0 + flowDuration)); + return app; + }; + makeFlow(0, highPort); // host0 -> HIGH + makeFlow(1, lowPort); // host1 -> LOW + + Simulator::Stop(Seconds(2.0 + flowDuration + 1.0)); + Simulator::Run(); + + // ---- Capture results while the core / TM still exist ---- + const uint64_t highRx = DynamicCast(highSinkApp.Get(0))->GetTotalRx(); + const uint64_t lowRx = DynamicCast(lowSinkApp.Get(0))->GetTotalRx(); + + uint64_t txPrioHigh = 0; + uint64_t txPrioLow = 0; + uint64_t tmReceived = 0; + uint64_t tmDropped = 0; + P4CoreV1model* core = sw->GetV1ModelCore(); + Ptr tm = core ? core->GetTrafficManager() : nullptr; + if (tm) + { + const auto& s = tm->GetStats(); + txPrioHigh = s.perPriorityTransmitted[3]; + txPrioLow = s.perPriorityTransmitted[1]; + tmReceived = s.totalReceived; + tmDropped = s.totalDropped; + } + + Simulator::Destroy(); + + // ---- Results ---- + const double highMbps = highRx * 8.0 / flowDuration / 1e6; + const double lowMbps = lowRx * 8.0 / flowDuration / 1e6; + const double offeredMbps = perFlowBps / 1e6; + const double highRetained = (offeredMbps > 0) ? (highMbps / offeredMbps) : 0.0; + + std::cout << std::fixed << std::setprecision(2) + << " HIGH: rx=" << highRx << " B ~" << highMbps << " Mbps (offered ~" + << offeredMbps << " Mbps, retained " << (highRetained * 100.0) << "%)\n" + << " LOW : rx=" << lowRx << " B ~" << lowMbps << " Mbps (offered ~" << offeredMbps + << " Mbps)\n" + << " [TM] received=" << tmReceived << " transmitted prio3=" << txPrioHigh + << " prio1=" << txPrioLow << " dropped=" << tmDropped << "\n"; + + Check(tm != nullptr, "Traffic Manager active on the switch"); + Check(highRx > 0 && lowRx > 0, "Both priority classes carried some traffic"); + Check(highRx > lowRx, "HIGH priority delivered more than LOW under congestion"); + Check(txPrioHigh > txPrioLow, "TM transmitted more prio-3 frames than prio-1 frames"); + Check(highRetained >= protectThreshold, "HIGH priority protected (retained offered load)"); + Check(tmDropped > 0, "TM dropped the excess low-priority load (port oversubscribed)"); + + std::cout << "=== " << (g_failures == 0 ? "STRICT PRIORITY OBSERVED" : "CHECKS FAILED") << " (" + << g_failures << " failure(s)) ===\n"; + return g_failures == 0 ? 0 : 1; +} diff --git a/examples/p4-voq-fabric-throughput.cc b/examples/p4-voq-fabric-throughput.cc new file mode 100644 index 0000000..b3c13b4 --- /dev/null +++ b/examples/p4-voq-fabric-throughput.cc @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025 TU Dresden + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation; + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Authors: Vineet Goel + */ + +/** + * Throughput benchmark for the VOQ + fabric Traffic Manager datapath. + * + * A single saturating UDP flow (offered load above link capacity) is sent + * host0 -> host1 through a V1model switch running IPv4 forwarding, and the + * goodput received at the sink is compared against the link's line rate. + * + * Because the VOQ datapath serialises each egress port at the port's own line + * rate (PortRate, seeded from the channel) and is driven by the transmit + * completion signal rather than a fixed timer, the delivered goodput should + * sit just below line rate (the small gap is Ethernet/IP/UDP header overhead), + * with no artificial timer bottleneck. + * + * One link rate per invocation (bmv2 cannot be re-initialised in a process): + * ./ns3 run "p4-voq-fabric-throughput --linkRate=100Mbps" + * ./ns3 run "p4-voq-fabric-throughput --linkRate=1000Mbps" + * + * Exit code 0 = goodput reached the near-line-rate threshold; non-zero = below. + */ + +#include "ns3/applications-module.h" +#include "ns3/core-module.h" +#include "ns3/data-rate.h" +#include "ns3/format-utils.h" +#include "ns3/internet-module.h" +#include "ns3/network-module.h" +#include "ns3/switched-ethernet-channel.h" +#include "ns3/p4-core-v1model.h" +#include "ns3/p4-helper.h" +#include "ns3/p4-switch-net-device.h" +#include "ns3/p4-traffic-manager.h" +#include "ns3/packet-sink.h" +#include "ns3/switched-ethernet-helper.h" + +#include +#include +#include +#include + +using namespace ns3; + +NS_LOG_COMPONENT_DEFINE("P4VoqFabricThroughput"); + +namespace +{ + +uint64_t g_rxBytes = 0; +double g_firstRx = -1.0; +double g_lastRx = 0.0; + +void +RxTrace(uint32_t payloadSize, Ptr pkt, const Address&) +{ + // Count only full data packets (skip stray/short frames). + if (pkt->GetSize() != payloadSize) + { + return; + } + double now = Simulator::Now().GetSeconds(); + if (g_firstRx < 0.0) + { + g_firstRx = now; + } + g_lastRx = now; + g_rxBytes += pkt->GetSize(); +} + +} // namespace + +int +main(int argc, char* argv[]) +{ + std::string linkRate = "1000Mbps"; // egress (bottleneck) link switch -> host1 + std::string hostLinkRate = "10Gbps"; // ingress link host0 -> switch (kept fast) + uint32_t pktSize = 1400; // UDP payload bytes + double offeredFactor = 1.2; // offered load = offeredFactor * linkRate + double flowDuration = 0.2; // seconds of saturating traffic + bool voq = true; // use the VOQ + fabric datapath + double passThreshold = 0.80; // PASS if goodput >= threshold * linkRate + + CommandLine cmd; + cmd.AddValue("linkRate", "Egress (bottleneck) link rate switch->host1", linkRate); + cmd.AddValue("hostLinkRate", "Ingress link rate host0->switch (kept fast)", hostLinkRate); + cmd.AddValue("pktSize", "UDP payload size in bytes", pktSize); + cmd.AddValue("offeredFactor", "Offered load as a multiple of the egress link rate", offeredFactor); + cmd.AddValue("flowDuration", "Duration of the saturating flow (s)", flowDuration); + cmd.AddValue("voq", "Use the VOQ+fabric datapath (else legacy)", voq); + cmd.AddValue("passThreshold", "PASS if goodput >= threshold * linkRate", passThreshold); + cmd.Parse(argc, argv); + + const uint64_t linkBps = DataRate(linkRate).GetBitRate(); + const uint64_t offeredBps = static_cast(linkBps * offeredFactor); + std::ostringstream offeredRate; + offeredRate << offeredBps << "bps"; + + // The ingress link host0->switch is kept fast so the host NIC (which has no + // tx queue and drops on a busy channel) never becomes the limiter; the + // switch's egress port switch->host1 is the sole bottleneck under test. + std::cout << "=== VOQ+fabric throughput benchmark ===\n" + << " egressLink=" << linkRate << " ingressLink=" << hostLinkRate + << " datapath=" << (voq ? "VOQ+fabric" : "legacy") << " pktSize=" << pktSize + << " offered=" << offeredFactor << "x egress line\n"; + + // ---- Topology: host0 -> switch -> host1 ---- + NodeContainer terminals; + terminals.Create(2); + Ptr switchNode = CreateObject(); + + InternetStackHelper internet; + internet.Install(terminals); + internet.Install(switchNode); + + Ipv4AddressHelper ipv4Addr; + ipv4Addr.SetBase("10.1.1.0", "255.255.255.0"); + + const std::string p4Dir = GetP4ExamplePath() + "/simple_v1model"; + + P4Helper p4; + p4.SetDeviceAttribute("JsonPath", StringValue(p4Dir + "/simple_v1model.json")); + p4.SetDeviceAttribute("FlowTablePath", StringValue(p4Dir + "/flowtable_0.txt")); + p4.SetDeviceAttribute("P4SwitchArch", UintegerValue(0)); + p4.SetDeviceAttribute("SwitchRate", UintegerValue(10000)); + p4.SetDeviceAttribute("EnableVoqFabric", BooleanValue(voq)); + Ptr sw = DynamicCast(p4.Install(switchNode).Get(0)); + + SwitchedEthernetHelper eth; + eth.SetChannelAttribute("DataRate", StringValue(hostLinkRate)); + eth.SetChannelAttribute("Delay", StringValue("1us")); + NetDeviceContainer hostDevs = eth.Install(sw, terminals); + + for (uint32_t i = 0; i < terminals.GetN(); ++i) + { + std::ostringstream mac; + mac << "00:00:00:00:00:" << std::hex << std::setfill('0') << std::setw(2) << (i + 1); + hostDevs.Get(i)->SetAddress(Mac48Address(mac.str().c_str())); + ipv4Addr.Assign(hostDevs.Get(i)); + } + + // Slow down only the egress link switch(port 1) -> host1 so it is the + // bottleneck; the ingress link keeps the fast hostLinkRate set above. + Ptr egressCh = sw->GetPortChannel(1); + egressCh->SetAttribute("DataRate", DataRateValue(DataRate(linkRate))); + + // ---- Saturating UDP flow host0 -> host1 ---- + const uint16_t serverPort = 9000; + Ipv4Address serverAddr = terminals.Get(1)->GetObject()->GetAddress(1, 0).GetLocal(); + + PacketSinkHelper sink("ns3::UdpSocketFactory", + InetSocketAddress(Ipv4Address::GetAny(), serverPort)); + ApplicationContainer sinkApp = sink.Install(terminals.Get(1)); + sinkApp.Start(Seconds(1.0)); + sinkApp.Stop(Seconds(2.0 + flowDuration + 1.0)); + + OnOffHelper onOff("ns3::UdpSocketFactory", InetSocketAddress(serverAddr, serverPort)); + onOff.SetAttribute("PacketSize", UintegerValue(pktSize)); + onOff.SetAttribute("DataRate", StringValue(offeredRate.str())); + onOff.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1]")); + onOff.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0]")); + ApplicationContainer clientApp = onOff.Install(terminals.Get(0)); + clientApp.Start(Seconds(2.0)); + clientApp.Stop(Seconds(2.0 + flowDuration)); + + sinkApp.Get(0)->TraceConnectWithoutContext("Rx", MakeBoundCallback(&RxTrace, pktSize)); + + Simulator::Stop(Seconds(2.0 + flowDuration + 1.0)); + Simulator::Run(); + + if (voq) + { + P4CoreV1model* core = sw->GetV1ModelCore(); + Ptr tm = core ? core->GetTrafficManager() : nullptr; + if (tm) + { + const auto& s = tm->GetStats(); + std::cout << " [TM] received=" << s.totalReceived << " voqEnq=" << s.totalVoqEnqueued + << " transmitted=" << s.totalTransmitted << " dropped=" << s.totalDropped + << "\n"; + } + } + + Simulator::Destroy(); + + // ---- Results ---- + const double window = (g_lastRx > g_firstRx) ? (g_lastRx - g_firstRx) : flowDuration; + const double goodputMbps = (window > 0) ? (g_rxBytes * 8.0 / window / 1e6) : 0.0; + const double linkMbps = linkBps / 1e6; + const double pctOfLink = (linkMbps > 0) ? (goodputMbps / linkMbps * 100.0) : 0.0; + + // Header-overhead ceiling for reference: payload / (payload + Eth+IP+UDP). + const double ceilingPct = 100.0 * pktSize / (pktSize + 14 + 20 + 8); + + std::cout << std::fixed << std::setprecision(2) << " rxBytes=" << g_rxBytes + << " window=" << window << "s\n" + << " goodput=" << goodputMbps << " Mbps of " << linkMbps << " Mbps line (" + << pctOfLink << "% of line; header-overhead ceiling ~" << ceilingPct << "%)\n"; + + const bool pass = pctOfLink >= passThreshold * 100.0; + std::cout << " [" << (pass ? "PASS" : "FAIL") << "] goodput >= " << (passThreshold * 100.0) + << "% of line rate\n" + << "=== " << (pass ? "NEAR LINE RATE" : "BELOW THRESHOLD") << " ===\n"; + return pass ? 0 : 1; +} diff --git a/examples/p4src/qos/flowtable_priority.txt b/examples/p4src/qos/flowtable_priority.txt new file mode 100644 index 0000000..a26746b --- /dev/null +++ b/examples/p4src/qos/flowtable_priority.txt @@ -0,0 +1,14 @@ +table_set_default ipv4_nhop drop +table_set_default arp_simple drop +table_add ipv4_nhop ipv4_forward 0x0a010101 => 00:00:00:00:00:01 0x0 +table_add ipv4_nhop ipv4_forward 0x0a010102 => 00:00:00:00:00:03 0x1 +table_add ipv4_nhop ipv4_forward 0x0a010103 => 00:00:00:00:00:05 0x2 +table_add arp_simple set_arp_nhop 0x0a010101 => 0x0 +table_add arp_simple set_arp_nhop 0x0a010102 => 0x1 +table_add arp_simple set_arp_nhop 0x0a010103 => 0x2 +table_add udp_priority set_priority 2000 => 0x1 +table_add udp_priority set_priority 3000 => 0x2 +table_add udp_priority set_priority 4000 => 0x3 +table_add tcp_priority set_priority 2000 => 0x1 +table_add tcp_priority set_priority 3000 => 0x2 +table_add tcp_priority set_priority 4000 => 0x3 diff --git a/model/p4-core-v1model.cc b/model/p4-core-v1model.cc index 9661360..ad167f3 100644 --- a/model/p4-core-v1model.cc +++ b/model/p4-core-v1model.cc @@ -20,6 +20,7 @@ #include "ns3/p4-core-v1model.h" +#include "ns3/boolean.h" #include "ns3/data-rate.h" #include "ns3/p4-switch-net-device.h" #include "ns3/p4-switch-queue-item.h" @@ -27,11 +28,13 @@ #include "ns3/register-access-v1model.h" #include "ns3/simulator.h" #include "ns3/switched-ethernet-channel.h" +#include "ns3/uinteger.h" #include "p4-switch-net-device.h" #include #include // tracing info to file #include +#include NS_LOG_COMPONENT_DEFINE("P4CoreV1model"); @@ -77,6 +80,26 @@ REGISTER_HASH(bmv2_hash_v1model); extern int import_primitives(); +// --------------------------------------------------------------------------- +// bm::Packet payload wrapper for the Traffic Manager. +// +// The Traffic Manager carries opaque, move-only TmPayload objects and never +// inspects them (preserving the packet-format boundary). BmPacketPayload is +// the concrete payload used by the real switch core: it owns the bm::Packet +// while it sits in the VOQ / egress queues. TmTransmit() downcasts back to +// recover the bm::Packet when the packet is serialised onto the wire. +// --------------------------------------------------------------------------- +class BmPacketPayload : public TmPayload +{ + public: + explicit BmPacketPayload(std::unique_ptr pkt) + : m_packet(std::move(pkt)) + { + } + + std::unique_ptr m_packet; +}; + P4CoreV1model::P4CoreV1model(P4SwitchNetDevice* net_device, bool enable_swap, bool enableTracing, @@ -121,6 +144,15 @@ P4CoreV1model::~P4CoreV1model() { NS_LOG_FUNCTION(this << " Destructing P4CoreV1model..."); + // Dispose the Traffic Manager first: this cancels its pending fabric/egress + // events and clears its transmit callback (which captures this core), so no + // scheduled TM event can fire on a partially destroyed core. + if (m_trafficManager) + { + m_trafficManager->Dispose(); + m_trafficManager = nullptr; + } + if (input_buffer) { input_buffer->push_front(InputBuffer::PacketType::SENTINEL, nullptr); @@ -150,6 +182,38 @@ P4CoreV1model::start_and_return_() NS_LOG_DEBUG("Switch ID: " << m_p4SwitchId << " using event-driven egress scheduler" << " (queue rate = " << m_switchRate << " pps" << ", link rate = " << m_linkRateBps << " bps)"); + + // Construct the VOQ + fabric Traffic Manager now that the ports and link + // rate are known. Opt-in only; the legacy egress_buffer path is unaffected. + if (m_enableVoqFabric) + { + uint32_t nPorts = m_switchNetDevice ? m_switchNetDevice->GetNPorts() : 0u; + if (nPorts == 0) + { + NS_LOG_WARN("Switch ID: " << m_p4SwitchId + << " EnableVoqFabric set but no ports attached; " + "VOQ path stays disabled"); + m_enableVoqFabric = false; + } + else + { + m_trafficManager = CreateObject(); + m_trafficManager->SetAttribute("EventDriven", BooleanValue(true)); + // Completion-driven egress: the TM selects the next frame, but the + // channel/PHY decides when it has finished serialising and signals + // back via TmNotifyTxDone() -> NotifyEgressTxComplete(). + m_trafficManager->SetAttribute("EgressCompletionDriven", BooleanValue(true)); + m_trafficManager->SetAttribute("NumPorts", UintegerValue(nPorts)); + m_trafficManager->SetAttribute("PortRate", DataRateValue(DataRate(m_linkRateBps))); + m_trafficManager->SetTransmitCallback( + [this](uint32_t outPort, uint8_t priority, std::unique_ptr payload) { + this->TmTransmit(outPort, priority, std::move(payload)); + }); + NS_LOG_INFO("Switch ID: " << m_p4SwitchId << " VOQ + fabric Traffic Manager enabled (" + << nPorts << " ports, port rate = " << m_linkRateBps + << " bps)"); + } + } } void @@ -782,6 +846,25 @@ P4CoreV1model::HandleIngressPipeline() void P4CoreV1model::Enqueue(uint32_t egress_port, std::unique_ptr&& packet) { + // Opt-in VOQ + fabric path. Steer normal port traffic into the Traffic + // Manager; anything with an out-of-range port (e.g. CPU / drop ports beyond + // the VOQ's N*N matrix) falls through to the legacy output-queued path + // below. When m_enableVoqFabric is false this branch is never taken and the + // switch behaves exactly as before. + if (m_enableVoqFabric && m_trafficManager) + { + const uint32_t n = m_trafficManager->GetNumPorts(); + const uint32_t in_port = static_cast(packet->get_ingress_port()); + if (egress_port < n && in_port < n) + { + EnqueueToTrafficManager(egress_port, std::move(packet)); + return; + } + NS_LOG_DEBUG("VOQ path: port out of range (in=" << in_port << ", out=" << egress_port + << ", N=" << n + << "); using legacy egress_buffer"); + } + packet->set_egress_port(egress_port); bm::PHV* phv = packet->get_phv(); @@ -967,4 +1050,225 @@ P4CoreV1model::SetPortQueueDisc(uint32_t port, Ptr qd) m_portQueueDiscs[port] = qd; } +// --------------------------------------------------------------------------- +// VOQ + fabric Traffic Manager (opt-in) integration +// --------------------------------------------------------------------------- + +void +P4CoreV1model::SetEnableVoqFabric(bool enable) +{ + NS_LOG_FUNCTION(this << enable); + m_enableVoqFabric = enable; +} + +bool +P4CoreV1model::GetEnableVoqFabric() const +{ + return m_enableVoqFabric; +} + +Ptr +P4CoreV1model::GetTrafficManager() const +{ + return m_trafficManager; +} + +void +P4CoreV1model::EnqueueToTrafficManager(uint32_t egress_port, std::unique_ptr&& packet) +{ + NS_LOG_FUNCTION(this << egress_port); + + packet->set_egress_port(egress_port); + bm::PHV* phv = packet->get_phv(); + + // Priority 0..7 from the P4 program (7 = highest), matching the Traffic + // Manager's priority convention. Clamp defensively. + size_t priority = phv->has_field("intrinsic_metadata.priority") + ? phv->get_field("intrinsic_metadata.priority").get() + : 0u; + if (priority >= P4_TM_NUM_PRIORITIES) + { + priority = P4_TM_NUM_PRIORITIES - 1; + } + + const uint32_t in_port = static_cast(packet->get_ingress_port()); + const uint32_t size_bytes = static_cast(packet->get_data_size()); + + if (m_enableQueueingMetadata) + { + phv->get_field("queueing_metadata.enq_timestamp").set(GetTimeStamp()); + phv->get_field("queueing_metadata.enq_qdepth") + .set(m_trafficManager->VoqLength(in_port, egress_port, + static_cast(priority))); + } + + auto payload = std::make_unique(std::move(packet)); + bool accepted = m_trafficManager->EnqueueToVoq(std::move(payload), + size_bytes, + in_port, + egress_port, + static_cast(priority)); + if (!accepted) + { + NS_LOG_DEBUG("Traffic Manager dropped packet (in=" << in_port << ", out=" << egress_port + << ", prio=" << priority << ")"); + } +} + +void +P4CoreV1model::TmTransmit(uint32_t outPort, uint8_t priority, std::unique_ptr payload) +{ + NS_LOG_FUNCTION(this << outPort << static_cast(priority)); + + // Recover the bm::Packet handed to the Traffic Manager at enqueue time. + auto* wrapper = dynamic_cast(payload.get()); + if (!wrapper || !wrapper->m_packet) + { + NS_LOG_ERROR("TmTransmit: unexpected/empty payload, dropping"); + // Nothing to serialise: free the port immediately so egress advances. + Simulator::ScheduleNow(&P4CoreV1model::TmNotifyTxDone, this, outPort); + return; + } + std::unique_ptr bm_packet = std::move(wrapper->m_packet); + + // ---- Run the egress pipeline on the serialised packet ---- + bm::PHV* phv = bm_packet->get_phv(); + bm::Pipeline* egress_mau = this->get_pipeline("egress"); + bm::Deparser* deparser = this->get_deparser("deparser"); + + if (phv->has_field("intrinsic_metadata.egress_global_timestamp")) + { + phv->get_field("intrinsic_metadata.egress_global_timestamp").set(GetTimeStamp()); + } + + if (m_enableQueueingMetadata) + { + uint64_t enq_timestamp = phv->get_field("queueing_metadata.enq_timestamp").get(); + phv->get_field("queueing_metadata.deq_timedelta").set(GetTimeStamp() - enq_timestamp); + phv->get_field("queueing_metadata.deq_qdepth") + .set(m_trafficManager->EgressLength(outPort, priority)); + if (phv->has_field("queueing_metadata.qid") && priority < m_nbQueuesPerPort) + { + phv->get_field("queueing_metadata.qid").set(m_nbQueuesPerPort - 1 - priority); + } + } + + phv->get_field("standard_metadata.egress_port").set(outPort); + + bm::Field& f_egress_spec = phv->get_field("standard_metadata.egress_spec"); + f_egress_spec.set(0); + + phv->get_field("standard_metadata.packet_length") + .set(bm_packet->get_register(RegisterAccess::PACKET_LENGTH_REG_IDX)); + + egress_mau->apply(bm_packet.get()); + + // EGRESS CLONING + auto clone_mirror_session_id = RegisterAccess::get_clone_mirror_session_id(bm_packet.get()); + auto clone_field_list = RegisterAccess::get_clone_field_list(bm_packet.get()); + if (clone_mirror_session_id) + { + NS_LOG_DEBUG("Cloning packet at egress, Packet ID: " << bm_packet->get_packet_id()); + RegisterAccess::set_clone_mirror_session_id(bm_packet.get(), 0); + RegisterAccess::set_clone_field_list(bm_packet.get(), 0); + MirroringSessionConfig config; + clone_mirror_session_id &= RegisterAccess::MIRROR_SESSION_ID_MASK; + bool is_session_configured = + GetMirroringSession(static_cast(clone_mirror_session_id), &config); + if (is_session_configured) + { + std::unique_ptr packet_copy = + bm_packet->clone_with_phv_reset_metadata_ptr(); + bm::PHV* phv_copy = packet_copy->get_phv(); + bm::FieldList* field_list = this->get_field_list(clone_field_list); + field_list->copy_fields_between_phvs(phv_copy, phv); + phv_copy->get_field("standard_metadata.instance_type") + .set(PKT_INSTANCE_TYPE_EGRESS_CLONE); + auto packet_size = bm_packet->get_register(RegisterAccess::PACKET_LENGTH_REG_IDX); + RegisterAccess::clear_all(packet_copy.get()); + packet_copy->set_register(RegisterAccess::PACKET_LENGTH_REG_IDX, packet_size); + if (config.mgid_valid) + { + NS_LOG_DEBUG("Cloning packet to MGID " << config.mgid); + MulticastPacket(packet_copy.get(), config.mgid); + } + if (config.egress_port_valid) + { + NS_LOG_DEBUG("Cloning packet to egress port " << config.egress_port); + Enqueue(config.egress_port, std::move(packet_copy)); + } + } + } + + uint32_t egress_spec = f_egress_spec.get_uint(); + if (egress_spec == m_dropPort) + { + NS_LOG_DEBUG("Dropping packet at the end of egress (VOQ path)"); + // Dropped at egress: no wire time, free the port immediately. + Simulator::ScheduleNow(&P4CoreV1model::TmNotifyTxDone, this, outPort); + return; + } + + deparser->deparse(bm_packet.get()); + + // RECIRCULATE + auto recirculate_flag = RegisterAccess::get_recirculate_flag(bm_packet.get()); + if (recirculate_flag) + { + NS_LOG_DEBUG("Recirculating packet (VOQ path)"); + int field_list_id = recirculate_flag; + RegisterAccess::set_recirculate_flag(bm_packet.get(), 0); + bm::FieldList* field_list = this->get_field_list(field_list_id); + std::unique_ptr packet_copy = bm_packet->clone_no_phv_ptr(); + bm::PHV* phv_copy = packet_copy->get_phv(); + phv_copy->reset_metadata(); + field_list->copy_fields_between_phvs(phv_copy, phv); + phv_copy->get_field("standard_metadata.instance_type").set(PKT_INSTANCE_TYPE_RECIRC); + size_t packet_size = packet_copy->get_data_size(); + RegisterAccess::clear_all(packet_copy.get()); + packet_copy->set_register(RegisterAccess::PACKET_LENGTH_REG_IDX, packet_size); + phv_copy->get_field("standard_metadata.packet_length").set(packet_size); + packet_copy->set_ingress_length(packet_size); + input_buffer->push_front(InputBuffer::PacketType::RECIRCULATE, std::move(packet_copy)); + HandleIngressPipeline(); + // Recirculated, not transmitted: free the port immediately. + Simulator::ScheduleNow(&P4CoreV1model::TmNotifyTxDone, this, outPort); + return; + } + + // Convert to an ns-3 packet and hand it to the NetDevice. The Traffic + // Manager chose this frame; the channel/PHY now decides its serialisation + // time and we report completion back so the TM can serve the next frame. + uint16_t protocol = RegisterAccess::get_ns_protocol(bm_packet.get()); + int addr_index = RegisterAccess::get_ns_address(bm_packet.get()); + Ptr ns_packet = this->ConvertToNs3Packet(std::move(bm_packet)); + uint32_t frameBytes = ns_packet->GetSize(); + + NS_LOG_DEBUG("VOQ path TX to NS-3 stack, Packet ID: " << ns_packet->GetUid() + << ", Size: " << frameBytes + << " bytes, Port: " << outPort); + + m_switchNetDevice->SendNs3Packet(ns_packet, + static_cast(outPort), + protocol, + m_destinationList[addr_index]); + + // Serialisation time is set by the port's channel (the PHY), not by the TM. + // Report completion when the last bit has been serialised; the sender is + // then free to send the next frame (the channel allows this while the frame + // is still propagating). + Ptr ch = m_switchNetDevice->GetPortChannel(outPort); + Time txTime = ch ? ch->GetDataRate().CalculateBytesTxTime(frameBytes) : Time(0); + Simulator::Schedule(txTime, &P4CoreV1model::TmNotifyTxDone, this, outPort); +} + +void +P4CoreV1model::TmNotifyTxDone(uint32_t outPort) +{ + if (m_trafficManager) + { + m_trafficManager->NotifyEgressTxComplete(outPort, true); + } +} + } // namespace ns3 \ No newline at end of file diff --git a/model/p4-core-v1model.h b/model/p4-core-v1model.h index 9931430..08fcf31 100644 --- a/model/p4-core-v1model.h +++ b/model/p4-core-v1model.h @@ -24,6 +24,7 @@ #include "ns3/p4-queue.h" #include "ns3/p4-switch-core.h" #include "ns3/p4-switch-queue-item.h" +#include "ns3/p4-traffic-manager.h" #include "ns3/queue-disc.h" #include "ns3/switched-ethernet-channel.h" @@ -84,6 +85,28 @@ class P4CoreV1model : public P4SwitchCore void PortTxComplete(uint32_t port); void TryTransmitFromQueueDisc(uint32_t port); + // === Optional VOQ + fabric Traffic Manager (opt-in, default OFF) === + // + // Additive integration layer. When disabled (the default), the switch uses + // the legacy output-queued path (egress_buffer + event-driven dequeue) and + // NONE of the methods below have any effect. When enabled via + // SetEnableVoqFabric(true) before the switch starts, packets leaving the + // ingress pipeline are steered into a P4TrafficManager (VOQ -> priority-first + // fabric -> strict-priority egress) instead of egress_buffer. The legacy + // path is left fully intact for side-by-side comparison / review. + + /** + * @brief Enable or disable the VOQ + fabric Traffic Manager path. + * + * Must be called before start_and_return_() (the TM is constructed there, + * once the ports/link rate are known). Default is disabled. + */ + void SetEnableVoqFabric(bool enable); + bool GetEnableVoqFabric() const; + + /** @brief The Traffic Manager instance, or nullptr if the VOQ path is off. */ + Ptr GetTrafficManager() const; + // === Per-port QueueDisc === /** @@ -171,6 +194,28 @@ class P4CoreV1model : public P4SwitchCore /// Physical link rate read from port 0 at startup; used for logging/diagnostics only. /// Actual serialisation delay is now modelled by the port NetDevice itself. uint64_t m_linkRateBps{1000000000ULL}; + + // ---- VOQ + fabric Traffic Manager (opt-in) ---- + + /// When true, HandleIngressPipeline's Enqueue() steers packets into m_trafficManager + /// instead of egress_buffer. Default false keeps the legacy output-queued path. + bool m_enableVoqFabric{false}; + + /// The Traffic Manager (VOQ + fabric + egress). Null unless m_enableVoqFabric. + Ptr m_trafficManager; + + /// Steer a post-ingress bm::Packet into the Traffic Manager's VOQ. + void EnqueueToTrafficManager(uint32_t egress_port, std::unique_ptr&& packet); + + /// TransmitCallback target: run egress pipeline + deparse + send for a packet + /// the Traffic Manager has selected for transmission. + void TmTransmit(uint32_t outPort, uint8_t priority, std::unique_ptr payload); + + /// Datapath -> Traffic Manager completion hook. Scheduled by TmTransmit() + /// for the moment the output port finishes serialising the current frame; + /// forwards to P4TrafficManager::NotifyEgressTxComplete() so the TM counts + /// the frame and serves the next one. + void TmNotifyTxDone(uint32_t outPort); }; // class P4CoreV1model } // namespace ns3 diff --git a/model/p4-switch-net-device.cc b/model/p4-switch-net-device.cc index 91aa4e8..d266eb7 100644 --- a/model/p4-switch-net-device.cc +++ b/model/p4-switch-net-device.cc @@ -111,6 +111,14 @@ P4SwitchNetDevice::GetTypeId() MakeUintegerAccessor(&P4SwitchNetDevice::m_switchRate), MakeUintegerChecker()) + .AddAttribute("EnableVoqFabric", + "If true, a V1model switch routes post-ingress traffic through the " + "VOQ + fabric Traffic Manager instead of the legacy output queues. " + "Default false keeps the legacy output-queued datapath.", + BooleanValue(false), + MakeBooleanAccessor(&P4SwitchNetDevice::m_enableVoqFabric), + MakeBooleanChecker()) + .AddAttribute( "Mtu", "Maximum Transmission Unit.", @@ -221,6 +229,7 @@ P4SwitchNetDevice::DoInitialize() m_queueBufferSize); m_v1modelSwitch->InitializeSwitchFromP4Json(m_jsonPath); m_v1modelSwitch->LoadFlowTableToSwitch(m_flowTablePath); + m_v1modelSwitch->SetEnableVoqFabric(m_enableVoqFabric); m_v1modelSwitch->start_and_return_(); break; diff --git a/model/p4-switch-net-device.h b/model/p4-switch-net-device.h index ae866ff..f806951 100644 --- a/model/p4-switch-net-device.h +++ b/model/p4-switch-net-device.h @@ -274,6 +274,12 @@ class P4SwitchNetDevice : public NetDevice size_t m_queueBufferSize; uint64_t m_switchRate; + /// When true, a V1model core routes traffic through the VOQ + fabric + /// Traffic Manager instead of the legacy output queues (attribute + /// "EnableVoqFabric"; default false). Propagated to the core in + /// DoInitialize() before start_and_return_(). + bool m_enableVoqFabric{false}; + // ----------------------------------------------------------------------- // NetDevice state // ----------------------------------------------------------------------- diff --git a/model/switched-ethernet-channel.cc b/model/switched-ethernet-channel.cc index 99936e8..b179d9c 100644 --- a/model/switched-ethernet-channel.cc +++ b/model/switched-ethernet-channel.cc @@ -109,6 +109,8 @@ SwitchedEthernetChannel::SwitchedEthernetChannel() m_State[1] = IDLE_STATE; m_currentSrc[0] = 0; m_currentSrc[1] = 0; + m_propCount[0] = 0; + m_propCount[1] = 0; m_deviceList.clear(); } @@ -223,9 +225,12 @@ SwitchedEthernetChannel::TransmitStart(Ptr p, uint32_t srcId) NS_LOG_FUNCTION(this << p << srcId); NS_LOG_INFO("UID=" << p->GetUid()); - if (m_State[srcId] != IDLE_STATE) + // Refused only while the slot is still serialising a frame. A slot that is + // merely propagating earlier frames may start a new one (full-duplex serial + // link: bits keep flowing behind the frames already on the wire). + if (m_State[srcId] == TRANSMITTING_STATE) { - NS_LOG_WARN("TransmitStart: wire not IDLE for slot " << srcId); + NS_LOG_WARN("TransmitStart: slot " << srcId << " still serialising a frame"); return false; } if (!IsActive(srcId)) @@ -247,8 +252,9 @@ SwitchedEthernetChannel::TransmitEnd(uint32_t srcId) NS_LOG_FUNCTION(this << srcId); NS_ASSERT(m_State[srcId] == TRANSMITTING_STATE); - m_State[srcId] = PROPAGATING_STATE; - NS_LOG_LOGIC("Slot " << srcId << " -> PROPAGATING_STATE"); + // Serialisation is complete: the sender is free to start the next frame + // right away, even though this frame is still propagating to the far end. + m_State[srcId] = IDLE_STATE; if (!IsActive(m_currentSrc[srcId])) { @@ -256,6 +262,11 @@ SwitchedEthernetChannel::TransmitEnd(uint32_t srcId) return false; } + // This frame is now in flight (one more outstanding propagation on the slot). + m_propCount[srcId]++; + NS_LOG_LOGIC("Slot " << srcId << " serialisation complete; in-flight frames=" + << m_propCount[srcId]); + // Schedule delivery to every active device that is NOT the sender. for (uint32_t i = 0; i < m_deviceList.size(); ++i) { @@ -299,9 +310,13 @@ void SwitchedEthernetChannel::PropagationCompleteEvent(uint32_t srcId) { NS_LOG_FUNCTION(this << srcId); - NS_ASSERT(m_State[srcId] == PROPAGATING_STATE); - m_State[srcId] = IDLE_STATE; - NS_LOG_LOGIC("Slot " << srcId << " -> IDLE_STATE"); + // Retire one in-flight frame. We do NOT touch m_State here: the slot may + // already be serialising a newer frame (TRANSMITTING_STATE), and that must + // not be reset by an earlier frame finishing its propagation. + NS_ASSERT(m_propCount[srcId] > 0); + m_propCount[srcId]--; + NS_LOG_LOGIC("Slot " << srcId << " propagation complete; in-flight frames=" + << m_propCount[srcId]); } // --------------------------------------------------------------------------- @@ -311,7 +326,8 @@ SwitchedEthernetChannel::PropagationCompleteEvent(uint32_t srcId) bool SwitchedEthernetChannel::IsBusy(uint32_t deviceId) const { - return m_State[deviceId] != IDLE_STATE; + // Only serialisation blocks a new frame; propagation does not. + return m_State[deviceId] == TRANSMITTING_STATE; } bool @@ -327,7 +343,17 @@ SwitchedEthernetChannel::IsActive(uint32_t deviceId) const FullDuplexWireState SwitchedEthernetChannel::GetState(uint32_t deviceId) const { - return m_State[deviceId]; + // m_State only tracks serialisation; PROPAGATING is derived from the count + // of frames still in flight on the slot. + if (m_State[deviceId] == TRANSMITTING_STATE) + { + return TRANSMITTING_STATE; + } + if (m_propCount[deviceId] > 0) + { + return PROPAGATING_STATE; + } + return IDLE_STATE; } int32_t diff --git a/model/switched-ethernet-channel.h b/model/switched-ethernet-channel.h index 893899c..56e7c1f 100644 --- a/model/switched-ethernet-channel.h +++ b/model/switched-ethernet-channel.h @@ -164,20 +164,31 @@ class SwitchedEthernetChannel : public Channel /** * \brief Start transmitting packet \p p from slot \p srcId. * - * Marks the wire as TRANSMITTING_STATE for slot \p srcId. The caller - * must schedule TransmitEnd() after the appropriate serialisation delay. + * Marks the slot as TRANSMITTING_STATE (serialisation in progress). The + * caller must schedule TransmitEnd() after the serialisation delay. + * + * A slot may start a new frame as soon as the previous frame has finished + * serialising, even while earlier frames are still PROPAGATING to the far + * end: a full-duplex serial link keeps pumping new bits behind the ones + * already travelling down the wire. Transmission is therefore refused only + * while the slot is still serialising a frame (TRANSMITTING_STATE), not + * while it is merely PROPAGATING. * * \param p Packet to transmit (full Ethernet frame). * \param srcId Slot ID of the transmitting device. - * \return true if the wire was idle and the device is active. + * \return true if the slot was not mid-serialisation and the device is active. */ bool TransmitStart(Ptr p, uint32_t srcId); /** * \brief Signal end of serialisation for slot \p srcId. * - * Switches the wire to PROPAGATING_STATE and schedules delivery of the - * packet to the far-end device after the propagation delay. + * The slot's serialisation is complete, so the sender is immediately free + * to start the next frame (the slot leaves TRANSMITTING_STATE). The frame + * just serialised is now in flight: one more propagation becomes + * outstanding and delivery to the far-end device is scheduled after the + * propagation delay. GetState() reports PROPAGATING_STATE while any frame + * is still in flight on the slot. * * \param srcId Slot ID of the transmitting device. * \return true unless the source was detached before completion. @@ -185,8 +196,9 @@ class SwitchedEthernetChannel : public Channel bool TransmitEnd(uint32_t srcId); /** - * \brief Called after propagation delay; frees the wire and delivers the - * packet to the far-end P4SwitchNetDevice via its Receive() method. + * \brief Called after the propagation delay; retires one in-flight frame + * for slot \p srcId. Delivery to the far end was already scheduled + * in TransmitEnd(); this only clears the propagation bookkeeping. * \param srcId Slot ID that originated the transmission. */ void PropagationCompleteEvent(uint32_t srcId); @@ -195,7 +207,11 @@ class SwitchedEthernetChannel : public Channel // Queries // ----------------------------------------------------------------------- - /** \return true if slot \p deviceId is currently transmitting or propagating. */ + /** + * \return true if slot \p deviceId is mid-serialisation (TRANSMITTING_STATE) + * and therefore cannot start a new frame. A slot that is only + * PROPAGATING earlier frames is NOT busy: it can start a new frame. + */ bool IsBusy(uint32_t deviceId) const; /** \return true if slot \p deviceId is active. */ @@ -229,9 +245,15 @@ class SwitchedEthernetChannel : public Channel std::vector m_deviceList; ///< Attached device records (max 2) - Ptr m_currentPkt[2]; ///< Packet currently on the wire for each slot + Ptr m_currentPkt[2]; ///< Packet currently being serialised on each slot uint32_t m_currentSrc[2]; ///< Source slot ID for each wire - FullDuplexWireState m_State[2]; ///< Wire state for each slot + FullDuplexWireState m_State[2]; ///< Serialisation state: IDLE or TRANSMITTING only + + /// Number of frames still propagating on each slot (TransmitEnd -> its + /// PropagationCompleteEvent). Serialisation frees the slot immediately, so + /// several frames can be in flight at once; GetState() derives + /// PROPAGATING_STATE from a positive count. + uint32_t m_propCount[2]; }; } // namespace ns3 diff --git a/test/channel-state-test-suite.cc b/test/channel-state-test-suite.cc index 80ddeca..e7f25dc 100644 --- a/test/channel-state-test-suite.cc +++ b/test/channel-state-test-suite.cc @@ -264,8 +264,13 @@ void FullDuplexIndependenceTest::DoRun() { Simulator::Schedule(MicroSeconds(5) + NanoSeconds(1), [this, ch]() { NS_TEST_ASSERT_MSG_EQ(ch->GetState(0), IDLE_STATE, "Slot 0 should be IDLE after prop"); - // Slot 1 still propagating (started prop at ~4 µs + 100 ns). - NS_TEST_ASSERT_MSG_EQ(ch->IsBusy(1), true, "Slot 1 should still be busy"); + // Slot 1 finished serialising at ~4 µs + 100 ns and is still propagating + // (until ~5 µs + 100 ns). Propagation does not make the sender busy, so + // IsBusy() is false, but GetState() still reports PROPAGATING. + NS_TEST_ASSERT_MSG_EQ(ch->IsBusy(1), false, + "Slot 1 is not busy while only propagating"); + NS_TEST_ASSERT_MSG_EQ(ch->GetState(1), PROPAGATING_STATE, + "Slot 1 should still be propagating"); }); Simulator::Run(); @@ -346,10 +351,12 @@ void PortDeviceIdAccessorTest::DoRun() { } // =========================================================================== -// Test 4: TransmitStart fails on a busy wire +// Test 4: TransmitStart is blocked only while serialising // -// Verifies that a second TransmitStart on the same slot returns false -// while the wire is TRANSMITTING or PROPAGATING. +// A slot refuses a new frame only while it is still serialising (TRANSMITTING). +// Once serialisation is done it may start the next frame immediately, even +// while an earlier frame is still PROPAGATING to the far end (full-duplex +// serial link). // =========================================================================== class TransmitStartBusyTest : public TestCase { @@ -361,7 +368,7 @@ class TransmitStartBusyTest : public TestCase { }; TransmitStartBusyTest::TransmitStartBusyTest() - : TestCase("TransmitStart returns false when wire is busy") {} + : TestCase("TransmitStart blocked only while serialising, not while propagating") {} void TransmitStartBusyTest::DoRun() { Ptr nodeA = CreateObject(); @@ -407,22 +414,27 @@ void TransmitStartBusyTest::DoRun() { // 200 bytes at 100 Mbps = 16 µs. Call TransmitEnd. Simulator::Schedule(MicroSeconds(16), [ch]() { ch->TransmitEnd(0); }); - // Try TransmitStart while PROPAGATING → should also fail. + // While PROPAGATING, a new TransmitStart is now allowed: serialisation is + // done so the sender is free even though the first frame is still in flight. Simulator::Schedule(MicroSeconds(16) + NanoSeconds(1), [this, ch, pkt2]() { NS_TEST_ASSERT_MSG_EQ(ch->GetState(0), PROPAGATING_STATE, - "Should be PROPAGATING"); + "Should be PROPAGATING after serialisation"); bool ok = ch->TransmitStart(pkt2, 0); - NS_TEST_ASSERT_MSG_EQ(ok, false, - "TransmitStart should fail while PROPAGATING"); + NS_TEST_ASSERT_MSG_EQ(ok, true, + "TransmitStart should succeed while only PROPAGATING"); + // pkt2 is now serialising, so the slot is TRANSMITTING again. + NS_TEST_ASSERT_MSG_EQ(ch->GetState(0), TRANSMITTING_STATE, + "Slot should be TRANSMITTING pkt2"); }); - // After propagation (16 µs + 5 µs = 21 µs), should be IDLE again. - Simulator::Schedule(MicroSeconds(21) + NanoSeconds(1), [this, ch, pkt2]() { - NS_TEST_ASSERT_MSG_EQ(ch->GetState(0), IDLE_STATE, - "Should be IDLE after propagation"); - bool ok = ch->TransmitStart(pkt2, 0); - NS_TEST_ASSERT_MSG_EQ(ok, true, - "TransmitStart should succeed once IDLE again"); + // While pkt2 is serialising, a further start must fail (mid-serialisation). + Simulator::Schedule(MicroSeconds(16) + NanoSeconds(100), + [this, ch, pkt1]() { + NS_TEST_ASSERT_MSG_EQ(ch->GetState(0), TRANSMITTING_STATE, + "Still serialising pkt2"); + bool ok = ch->TransmitStart(pkt1, 0); + NS_TEST_ASSERT_MSG_EQ(ok, false, + "TransmitStart should fail while serialising pkt2"); }); Simulator::Run(); diff --git a/test/p4-traffic-manager-test-suite.cc b/test/p4-traffic-manager-test-suite.cc new file mode 100644 index 0000000..ba8e58d --- /dev/null +++ b/test/p4-traffic-manager-test-suite.cc @@ -0,0 +1,547 @@ +/* + * Copyright (c) 2025 TU Dresden + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation; + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Authors: Vineet Goel + */ + +#include "ns3/boolean.h" +#include "ns3/data-rate.h" +#include "ns3/nstime.h" +#include "ns3/p4-traffic-manager.h" +#include "ns3/simulator.h" +#include "ns3/test.h" +#include "ns3/uinteger.h" + +#include +#include +#include + +using namespace ns3; + +namespace +{ + +/// Lightweight test payload so the TM can be exercised without a bm::Packet. +class DummyPayload : public TmPayload +{ + public: + explicit DummyPayload(uint32_t tag) + : m_tag(tag) + { + } + + uint32_t GetTag() const + { + return m_tag; + } + + private: + uint32_t m_tag; +}; + +/// Convenience: make a payload and enqueue it into the VOQ. +bool +Enq(Ptr tm, + uint32_t size, + uint32_t in, + uint32_t out, + uint8_t prio, + uint32_t tag = 0) +{ + return tm->EnqueueToVoq(std::make_unique(tag), size, in, out, prio); +} + +Ptr +MakeTm(uint32_t numPorts) +{ + Ptr tm = CreateObject(); + tm->SetNumPorts(numPorts); + return tm; +} + +} // namespace + +// --------------------------------------------------------------------------- +// 1. Basic enqueue / dequeue correctness + VOQ sizing +// --------------------------------------------------------------------------- +class TmBasicEnqueueDequeueTest : public TestCase +{ + public: + TmBasicEnqueueDequeueTest() + : TestCase("TM: basic enqueue/dequeue and accounting") + { + } + + void DoRun() override + { + Ptr tm = MakeTm(4); + + NS_TEST_ASSERT_MSG_EQ(tm->GetNumPorts(), 4, "NumPorts should be 4"); + NS_TEST_ASSERT_MSG_EQ(tm->GlobalBufferBytes(), 0, "global bytes start at 0"); + + NS_TEST_ASSERT_MSG_EQ(Enq(tm, 100, 0, 1, 3, 42), true, "enqueue should succeed"); + NS_TEST_ASSERT_MSG_EQ(tm->VoqLength(0, 1, 3), 1, "VOQ[0][1][3] len == 1"); + NS_TEST_ASSERT_MSG_EQ(tm->VoqBytes(0, 1, 3), 100, "VOQ bytes == 100"); + NS_TEST_ASSERT_MSG_EQ(tm->InputBufferBytes(0), 100, "input bytes == 100"); + NS_TEST_ASSERT_MSG_EQ(tm->GlobalBufferBytes(), 100, "global bytes == 100"); + + // Second packet, same VOQ. + NS_TEST_ASSERT_MSG_EQ(Enq(tm, 200, 0, 1, 3, 43), true, "enqueue 2 should succeed"); + NS_TEST_ASSERT_MSG_EQ(tm->VoqLength(0, 1, 3), 2, "VOQ len == 2"); + NS_TEST_ASSERT_MSG_EQ(tm->GlobalBufferBytes(), 300, "global bytes == 300"); + + // FIFO order within a VOQ: first out is tag 42. + TmItem item; + NS_TEST_ASSERT_MSG_EQ(tm->DequeueFromVoq(0, 1, 3, item), true, "dequeue should succeed"); + auto* p = dynamic_cast(item.payload.get()); + NS_TEST_ASSERT_MSG_NE(p, nullptr, "payload must survive as DummyPayload"); + NS_TEST_ASSERT_MSG_EQ(p->GetTag(), 42, "FIFO: first dequeued tag == 42"); + NS_TEST_ASSERT_MSG_EQ(tm->VoqBytes(0, 1, 3), 200, "VOQ bytes back to 200"); + NS_TEST_ASSERT_MSG_EQ(tm->GlobalBufferBytes(), 200, "global bytes == 200"); + + // Dequeue empty VOQ returns false. + NS_TEST_ASSERT_MSG_EQ(tm->DequeueFromVoq(2, 2, 0, item), false, "empty VOQ dequeue false"); + + const auto& s = tm->GetStats(); + NS_TEST_ASSERT_MSG_EQ(s.totalReceived, 2, "received == 2"); + NS_TEST_ASSERT_MSG_EQ(s.totalVoqEnqueued, 2, "enqueued == 2"); + NS_TEST_ASSERT_MSG_EQ(s.totalMovedToEgress, 1, "moved to egress == 1"); + NS_TEST_ASSERT_MSG_EQ(s.totalDropped, 0, "no drops"); + + Simulator::Destroy(); + } +}; + +// --------------------------------------------------------------------------- +// 2. Priority scheduling correctness (fabric picks highest priority first) +// --------------------------------------------------------------------------- +class TmPrioritySchedulingTest : public TestCase +{ + public: + TmPrioritySchedulingTest() + : TestCase("TM: fabric serves higher priority first") + { + } + + void DoRun() override + { + Ptr tm = MakeTm(2); + + // Same input->output, different priorities: 2 and 5. + Enq(tm, 100, 0, 1, 2, 1); + Enq(tm, 100, 0, 1, 5, 2); + + auto grants = tm->RunFabricScheduler(); + NS_TEST_ASSERT_MSG_EQ(grants.size(), 1, "one input -> one grant per round"); + NS_TEST_ASSERT_MSG_EQ(static_cast(grants[0].priority), 5, + "priority 5 must be served before 2"); + NS_TEST_ASSERT_MSG_EQ(grants[0].inPort, 0, "granted input 0"); + NS_TEST_ASSERT_MSG_EQ(grants[0].outPort, 1, "granted output 1"); + + Simulator::Destroy(); + } +}; + +// --------------------------------------------------------------------------- +// 3. VOQ correctness: two inputs target the same output +// --------------------------------------------------------------------------- +class TmVoqSameOutputTest : public TestCase +{ + public: + TmVoqSameOutputTest() + : TestCase("TM: two inputs to same output are isolated VOQs") + { + } + + void DoRun() override + { + Ptr tm = MakeTm(3); + + // input 0 and input 2 both target output 1, same priority. + Enq(tm, 100, 0, 1, 4, 10); + Enq(tm, 100, 2, 1, 4, 20); + + // Distinct VOQs. + NS_TEST_ASSERT_MSG_EQ(tm->VoqLength(0, 1, 4), 1, "VOQ[0][1][4] len 1"); + NS_TEST_ASSERT_MSG_EQ(tm->VoqLength(2, 1, 4), 1, "VOQ[2][1][4] len 1"); + NS_TEST_ASSERT_MSG_EQ(tm->InputBufferBytes(0), 100, "input 0 bytes"); + NS_TEST_ASSERT_MSG_EQ(tm->InputBufferBytes(2), 100, "input 2 bytes"); + + // Output contention: only ONE of them can be granted this round + // (one output receives at most one packet per round). + auto grants = tm->RunFabricScheduler(); + NS_TEST_ASSERT_MSG_EQ(grants.size(), 1, "output 1 contended -> single grant"); + NS_TEST_ASSERT_MSG_EQ(grants[0].outPort, 1, "grant is for output 1"); + + Simulator::Destroy(); + } +}; + +// --------------------------------------------------------------------------- +// 4. Fabric matching rules: +// - one input sends at most one packet per round +// - one output receives at most one packet per round +// - a full permutation yields a maximal matching +// --------------------------------------------------------------------------- +class TmFabricMatchingTest : public TestCase +{ + public: + TmFabricMatchingTest() + : TestCase("TM: fabric one-in/one-out matching constraints") + { + } + + void DoRun() override + { + Ptr tm = MakeTm(4); + + // A perfect permutation demand: in i -> out (i+1)%4, all same priority. + for (uint32_t i = 0; i < 4; ++i) + { + Enq(tm, 100, i, (i + 1) % 4, 3, i); + } + + auto grants = tm->RunFabricScheduler(); + NS_TEST_ASSERT_MSG_EQ(grants.size(), 4, "permutation -> 4 grants (maximal)"); + + std::vector inSeen(4, false), outSeen(4, false); + for (const auto& g : grants) + { + NS_TEST_ASSERT_MSG_EQ(inSeen[g.inPort], false, "each input granted at most once"); + NS_TEST_ASSERT_MSG_EQ(outSeen[g.outPort], false, "each output granted at most once"); + inSeen[g.inPort] = true; + outSeen[g.outPort] = true; + } + + // Now a contention case: inputs 0,1,2 all want output 0. + Ptr tm2 = MakeTm(4); + Enq(tm2, 100, 0, 0, 3, 0); + Enq(tm2, 100, 1, 0, 3, 1); + Enq(tm2, 100, 2, 0, 3, 2); + auto g2 = tm2->RunFabricScheduler(); + NS_TEST_ASSERT_MSG_EQ(g2.size(), 1, "3 inputs -> 1 output => single grant"); + NS_TEST_ASSERT_MSG_EQ(g2[0].outPort, 0, "grant for output 0"); + // Priority-first, then input order: input 0 wins the tie. + NS_TEST_ASSERT_MSG_EQ(g2[0].inPort, 0, "input 0 wins tie (lowest index first)"); + + Simulator::Destroy(); + } +}; + +// --------------------------------------------------------------------------- +// 5. Buffer overflow and drop-reason correctness +// --------------------------------------------------------------------------- +class TmBufferDropTest : public TestCase +{ + public: + TmBufferDropTest() + : TestCase("TM: finite buffers produce correct drop reasons") + { + } + + void DoRun() override + { + // ---- VOQ limit ---- + { + Ptr tm = MakeTm(2); + tm->SetAttribute("VoqLimit", UintegerValue(150)); + + uint8_t lastReason = 255; + tm->TraceConnectWithoutContext( + "Drop", + MakeCallback(&TmBufferDropTest::OnDrop, this)); + m_lastReason = &lastReason; + + NS_TEST_ASSERT_MSG_EQ(Enq(tm, 100, 0, 1, 0), true, "first fits under VoqLimit"); + NS_TEST_ASSERT_MSG_EQ(Enq(tm, 100, 0, 1, 0), false, "second exceeds VoqLimit -> drop"); + NS_TEST_ASSERT_MSG_EQ(lastReason, + static_cast(TmDropReason::VOQ_QUEUE_FULL), + "drop reason == VOQ_QUEUE_FULL"); + NS_TEST_ASSERT_MSG_EQ(tm->VoqLength(0, 1, 0), 1, "only one packet accepted"); + const auto& s = tm->GetStats(); + NS_TEST_ASSERT_MSG_EQ(s.totalDropped, 1, "one drop counted"); + NS_TEST_ASSERT_MSG_EQ( + s.dropsByReason[static_cast(TmDropReason::VOQ_QUEUE_FULL)], + 1, + "VOQ_QUEUE_FULL counter == 1"); + m_lastReason = nullptr; + } + + // ---- Input buffer limit (independent of a single VOQ) ---- + { + Ptr tm = MakeTm(3); + tm->SetAttribute("InputBufferLimit", UintegerValue(150)); + uint8_t lastReason = 255; + tm->TraceConnectWithoutContext( + "Drop", + MakeCallback(&TmBufferDropTest::OnDrop, this)); + m_lastReason = &lastReason; + + // Two different VOQs of the same input; second overflows input budget. + NS_TEST_ASSERT_MSG_EQ(Enq(tm, 100, 0, 1, 0), true, "input budget ok"); + NS_TEST_ASSERT_MSG_EQ(Enq(tm, 100, 0, 2, 5), false, "input budget exceeded -> drop"); + NS_TEST_ASSERT_MSG_EQ(lastReason, + static_cast(TmDropReason::VOQ_INPUT_BUFFER_FULL), + "drop reason == VOQ_INPUT_BUFFER_FULL"); + m_lastReason = nullptr; + } + + // ---- Global buffer limit ---- + { + Ptr tm = MakeTm(3); + tm->SetAttribute("GlobalBufferLimit", UintegerValue(150)); + uint8_t lastReason = 255; + tm->TraceConnectWithoutContext( + "Drop", + MakeCallback(&TmBufferDropTest::OnDrop, this)); + m_lastReason = &lastReason; + + // Different inputs, different VOQs; global budget is the binding limit. + NS_TEST_ASSERT_MSG_EQ(Enq(tm, 100, 0, 1, 0), true, "global budget ok"); + NS_TEST_ASSERT_MSG_EQ(Enq(tm, 100, 1, 2, 0), false, "global budget exceeded -> drop"); + NS_TEST_ASSERT_MSG_EQ(lastReason, + static_cast(TmDropReason::VOQ_GLOBAL_BUFFER_FULL), + "drop reason == VOQ_GLOBAL_BUFFER_FULL"); + m_lastReason = nullptr; + } + + Simulator::Destroy(); + } + + private: + void OnDrop(uint8_t reason, uint32_t, uint32_t, uint8_t, uint32_t) + { + if (m_lastReason) + { + *m_lastReason = reason; + } + } + + uint8_t* m_lastReason{nullptr}; +}; + +// --------------------------------------------------------------------------- +// 6. Delay measurement correctness (VOQ waiting delay via simulated time) +// --------------------------------------------------------------------------- +class TmDelayMeasurementTest : public TestCase +{ + public: + TmDelayMeasurementTest() + : TestCase("TM: VOQ waiting delay is measured in simulated time") + { + } + + void DoRun() override + { + Ptr tm = MakeTm(2); + tm->TraceConnectWithoutContext("VoqWaitingDelay", + MakeCallback(&TmDelayMeasurementTest::OnDelay, this)); + + // Enqueue at t=0, dequeue at t=500ns. + Simulator::Schedule(Time(0), [tm]() { Enq(tm, 100, 0, 1, 3, 7); }); + Simulator::Schedule(NanoSeconds(500), [tm]() { + TmItem item; + tm->DequeueFromVoq(0, 1, 3, item); + }); + + Simulator::Run(); + + NS_TEST_ASSERT_MSG_EQ(m_lastDelay, NanoSeconds(500), "VOQ waiting delay == 500ns"); + const auto& s = tm->GetStats(); + NS_TEST_ASSERT_MSG_EQ(s.AvgVoqDelay(), NanoSeconds(500), "avg VOQ delay == 500ns"); + NS_TEST_ASSERT_MSG_EQ(s.maxQueueingDelay, NanoSeconds(500), "max queueing delay == 500ns"); + + Simulator::Destroy(); + } + + private: + void OnDelay(Time d) + { + m_lastDelay = d; + } + + Time m_lastDelay; +}; + +// --------------------------------------------------------------------------- +// 7. Event-driven end-to-end drain (P3 fabric loop + P4 egress scheduler) +// --------------------------------------------------------------------------- +class TmEventDrivenDrainTest : public TestCase +{ + public: + TmEventDrivenDrainTest() + : TestCase("TM: event-driven fabric+egress drains all packets to the wire") + { + } + + void DoRun() override + { + Ptr tm = MakeTm(4); + tm->SetAttribute("EventDriven", BooleanValue(true)); + tm->SetAttribute("PortRate", DataRateValue(DataRate("1Gbps"))); + tm->SetAttribute("FabricRate", DataRateValue(DataRate("10Gbps"))); + + uint32_t delivered = 0; + tm->SetTransmitCallback( + [&delivered](uint32_t, uint8_t, std::unique_ptr) { delivered++; }); + + // A perfect permutation: in i -> out (i+1)%4, so no egress contention. + for (uint32_t i = 0; i < 4; ++i) + { + Enq(tm, 100, i, (i + 1) % 4, 3, i); + } + + Simulator::Run(); + + NS_TEST_ASSERT_MSG_EQ(delivered, 4, "all 4 packets delivered via TransmitCallback"); + NS_TEST_ASSERT_MSG_EQ(tm->GlobalBufferBytes(), 0, "buffers empty after drain"); + for (uint32_t out = 0; out < 4; ++out) + { + NS_TEST_ASSERT_MSG_EQ(tm->EgressPortBytes(out), 0, "egress port drained"); + } + + const auto& s = tm->GetStats(); + NS_TEST_ASSERT_MSG_EQ(s.totalVoqEnqueued, 4, "4 enqueued"); + NS_TEST_ASSERT_MSG_EQ(s.totalMovedToEgress, 4, "4 moved to egress"); + NS_TEST_ASSERT_MSG_EQ(s.totalEgressEnqueued, 4, "4 accepted at egress"); + NS_TEST_ASSERT_MSG_EQ(s.totalTransmitted, 4, "4 transmitted"); + NS_TEST_ASSERT_MSG_EQ(s.totalDropped, 0, "no drops"); + + Simulator::Destroy(); + } +}; + +// --------------------------------------------------------------------------- +// 8. Egress strict-priority: a packet already in service is not preempted, +// but among waiting packets the highest priority goes next. +// --------------------------------------------------------------------------- +class TmEgressStrictPriorityTest : public TestCase +{ + public: + TmEgressStrictPriorityTest() + : TestCase("TM: egress scheduler serves waiting packets in strict priority") + { + } + + void DoRun() override + { + Ptr tm = MakeTm(4); + tm->SetAttribute("EventDriven", BooleanValue(true)); + // Slow port so the first packet is still transmitting when the others + // pile up behind it in the egress queue of output 0. + tm->SetAttribute("PortRate", DataRateValue(DataRate("10Mbps"))); + tm->SetAttribute("FabricRate", DataRateValue(DataRate("100Gbps"))); + + std::vector order; + tm->SetTransmitCallback( + [&order](uint32_t, uint8_t prio, std::unique_ptr) { order.push_back(prio); }); + + // All target output 0 (distinct inputs so the fabric can move them). + // Low-priority packet enters service first; higher priorities queue. + Simulator::Schedule(Time(0), [tm]() { Enq(tm, 100, 0, 0, 2, 2); }); + Simulator::Schedule(MicroSeconds(10), [tm]() { Enq(tm, 100, 1, 0, 5, 5); }); + Simulator::Schedule(MicroSeconds(20), [tm]() { Enq(tm, 100, 2, 0, 7, 7); }); + + Simulator::Run(); + + NS_TEST_ASSERT_MSG_EQ(order.size(), 3, "all three packets transmitted"); + // First to arrive (prio 2) is already on the wire; then strict priority. + NS_TEST_ASSERT_MSG_EQ(static_cast(order[0]), 2, "prio 2 in service first"); + NS_TEST_ASSERT_MSG_EQ(static_cast(order[1]), 7, "prio 7 next (highest waiting)"); + NS_TEST_ASSERT_MSG_EQ(static_cast(order[2]), 5, "prio 5 last"); + + Simulator::Destroy(); + } +}; + +// --------------------------------------------------------------------------- +// 9. Egress buffer overflow produces EGRESS_QUEUE_FULL +// --------------------------------------------------------------------------- +class TmEgressDropTest : public TestCase +{ + public: + TmEgressDropTest() + : TestCase("TM: full egress queue drops with EGRESS_QUEUE_FULL") + { + } + + void DoRun() override + { + Ptr tm = MakeTm(4); + tm->SetAttribute("EventDriven", BooleanValue(true)); + // Very slow port so egress never drains during the test; fast fabric. + tm->SetAttribute("PortRate", DataRateValue(DataRate("1Kbps"))); + tm->SetAttribute("FabricRate", DataRateValue(DataRate("100Gbps"))); + // Room for one 100B packet waiting behind the one in service. + tm->SetAttribute("EgressQueueLimit", UintegerValue(150)); + + tm->TraceConnectWithoutContext("Drop", MakeCallback(&TmEgressDropTest::OnDrop, this)); + + // Three packets, distinct inputs, same output+priority. The fabric moves + // them one per round: #1 enters service, #2 waits (100B <= 150), + // #3 overflows the egress queue (200B > 150) -> EGRESS_QUEUE_FULL. + Enq(tm, 100, 0, 0, 3, 0); + Enq(tm, 100, 1, 0, 3, 1); + Enq(tm, 100, 2, 0, 3, 2); + + Simulator::Run(); + + NS_TEST_ASSERT_MSG_EQ(m_egressDrops, 1, "exactly one egress drop"); + const auto& s = tm->GetStats(); + NS_TEST_ASSERT_MSG_EQ( + s.dropsByReason[static_cast(TmDropReason::EGRESS_QUEUE_FULL)], + 1, + "EGRESS_QUEUE_FULL counter == 1"); + NS_TEST_ASSERT_MSG_EQ(s.totalMovedToEgress, 3, "all three granted out of the VOQ"); + NS_TEST_ASSERT_MSG_EQ(s.totalEgressEnqueued, 2, "only two accepted into egress"); + + Simulator::Destroy(); + } + + private: + void OnDrop(uint8_t reason, uint32_t, uint32_t, uint8_t, uint32_t) + { + if (reason == static_cast(TmDropReason::EGRESS_QUEUE_FULL)) + { + m_egressDrops++; + } + } + + uint32_t m_egressDrops{0}; +}; + +// --------------------------------------------------------------------------- +// Suite +// --------------------------------------------------------------------------- +class P4TrafficManagerTestSuite : public TestSuite +{ + public: + P4TrafficManagerTestSuite() + : TestSuite("p4-traffic-manager", Type::UNIT) + { + AddTestCase(new TmBasicEnqueueDequeueTest, TestCase::QUICK); + AddTestCase(new TmPrioritySchedulingTest, TestCase::QUICK); + AddTestCase(new TmVoqSameOutputTest, TestCase::QUICK); + AddTestCase(new TmFabricMatchingTest, TestCase::QUICK); + AddTestCase(new TmBufferDropTest, TestCase::QUICK); + AddTestCase(new TmDelayMeasurementTest, TestCase::QUICK); + AddTestCase(new TmEventDrivenDrainTest, TestCase::QUICK); + AddTestCase(new TmEgressStrictPriorityTest, TestCase::QUICK); + AddTestCase(new TmEgressDropTest, TestCase::QUICK); + } +}; + +static P4TrafficManagerTestSuite g_p4TrafficManagerTestSuite; diff --git a/utils/p4-traffic-manager.cc b/utils/p4-traffic-manager.cc new file mode 100644 index 0000000..1d53b66 --- /dev/null +++ b/utils/p4-traffic-manager.cc @@ -0,0 +1,815 @@ +/* + * Copyright (c) 2025 TU Dresden + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation; + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Authors: Vineet Goel + */ + +#include "p4-traffic-manager.h" + +#include "ns3/abort.h" +#include "ns3/boolean.h" +#include "ns3/log.h" +#include "ns3/simulator.h" +#include "ns3/uinteger.h" + +#include + +namespace ns3 +{ + +NS_LOG_COMPONENT_DEFINE("P4TrafficManager"); + +NS_OBJECT_ENSURE_REGISTERED(P4TrafficManager); + +const char* +TmDropReasonToString(TmDropReason r) +{ + switch (r) + { + case TmDropReason::VOQ_GLOBAL_BUFFER_FULL: + return "VOQ_GLOBAL_BUFFER_FULL"; + case TmDropReason::VOQ_INPUT_BUFFER_FULL: + return "VOQ_INPUT_BUFFER_FULL"; + case TmDropReason::VOQ_QUEUE_FULL: + return "VOQ_QUEUE_FULL"; + case TmDropReason::EGRESS_PORT_BUFFER_FULL: + return "EGRESS_PORT_BUFFER_FULL"; + case TmDropReason::EGRESS_QUEUE_FULL: + return "EGRESS_QUEUE_FULL"; + } + return "UNKNOWN"; +} + +// --------------------------------------------------------------------------- +// TmStats helpers +// --------------------------------------------------------------------------- + +Time +P4TrafficManager::TmStats::AvgVoqDelay() const +{ + return (cntVoqDelay == 0) ? Time(0) : (sumVoqDelay / static_cast(cntVoqDelay)); +} + +Time +P4TrafficManager::TmStats::AvgEgressDelay() const +{ + return (cntEgressDelay == 0) ? Time(0) : (sumEgressDelay / static_cast(cntEgressDelay)); +} + +Time +P4TrafficManager::TmStats::AvgTotalDelay() const +{ + return (cntTotalDelay == 0) ? Time(0) : (sumTotalDelay / static_cast(cntTotalDelay)); +} + +// --------------------------------------------------------------------------- +// TypeId / construction +// --------------------------------------------------------------------------- + +TypeId +P4TrafficManager::GetTypeId() +{ + static TypeId tid = + TypeId("ns3::P4TrafficManager") + .SetParent() + .SetGroupName("P4sim") + .AddConstructor() + .AddAttribute("NumPorts", + "Number of switch ports N (allocates N*N*8 VOQs).", + UintegerValue(0), + MakeUintegerAccessor(&P4TrafficManager::SetNumPorts, + &P4TrafficManager::GetNumPorts), + MakeUintegerChecker()) + .AddAttribute("GlobalBufferLimit", + "Global buffer limit in bytes across VOQ and egress (0 = unlimited).", + UintegerValue(0), + MakeUintegerAccessor(&P4TrafficManager::m_globalBufferLimit), + MakeUintegerChecker()) + .AddAttribute("InputBufferLimit", + "Per-input-port buffer limit in bytes (0 = unlimited).", + UintegerValue(0), + MakeUintegerAccessor(&P4TrafficManager::m_inputBufferLimit), + MakeUintegerChecker()) + .AddAttribute("VoqLimit", + "Per-VOQ[in][out][prio] limit in bytes (0 = unlimited).", + UintegerValue(0), + MakeUintegerAccessor(&P4TrafficManager::m_voqLimit), + MakeUintegerChecker()) + .AddAttribute("EgressPortLimit", + "Per-output-port egress buffer limit in bytes (0 = unlimited).", + UintegerValue(0), + MakeUintegerAccessor(&P4TrafficManager::m_egressPortLimit), + MakeUintegerChecker()) + .AddAttribute("EgressQueueLimit", + "Per-egress-queue[out][prio] limit in bytes (0 = unlimited).", + UintegerValue(0), + MakeUintegerAccessor(&P4TrafficManager::m_egressQueueLimit), + MakeUintegerChecker()) + .AddAttribute("PortRate", + "Output-port serialization rate.", + DataRateValue(DataRate("1Gbps")), + MakeDataRateAccessor(&P4TrafficManager::m_portRate), + MakeDataRateChecker()) + .AddAttribute("FabricRate", + "Fabric transfer rate.", + DataRateValue(DataRate("10Gbps")), + MakeDataRateAccessor(&P4TrafficManager::m_fabricRate), + MakeDataRateChecker()) + .AddAttribute("IngressPipelineDelay", + "Fixed ingress pipeline processing delay.", + TimeValue(Time(0)), + MakeTimeAccessor(&P4TrafficManager::m_ingressPipelineDelay), + MakeTimeChecker()) + .AddAttribute("FabricArbitrationDelay", + "Fixed fabric arbitration delay per round.", + TimeValue(Time(0)), + MakeTimeAccessor(&P4TrafficManager::m_fabricArbitrationDelay), + MakeTimeChecker()) + .AddAttribute("EgressPipelineDelay", + "Fixed egress pipeline processing delay.", + TimeValue(Time(0)), + MakeTimeAccessor(&P4TrafficManager::m_egressPipelineDelay), + MakeTimeChecker()) + .AddAttribute("EventDriven", + "If true, EnqueueToVoq self-clocks the fabric + egress " + "scheduler via ns-3 events; if false, the fabric is driven " + "manually via RunFabricScheduler()/DequeueFromVoq().", + BooleanValue(false), + MakeBooleanAccessor(&P4TrafficManager::m_eventDriven), + MakeBooleanChecker()) + .AddAttribute("EgressCompletionDriven", + "If true (and EventDriven), the egress scheduler hands each " + "frame to the TransmitCallback and waits for " + "NotifyEgressTxComplete() before counting it transmitted and " + "serving the next frame (the datapath/PHY decides the timing). " + "If false, egress self-clocks serialisation at PortRate.", + BooleanValue(false), + MakeBooleanAccessor(&P4TrafficManager::m_egressCompletionDriven), + MakeBooleanChecker()) + .AddTraceSource("VoqEnqueue", + "A packet was enqueued into a VOQ (in, out, prio, bytes).", + MakeTraceSourceAccessor(&P4TrafficManager::m_voqEnqueueTrace), + "ns3::P4TrafficManager::QueueOpCallback") + .AddTraceSource("VoqDequeue", + "A packet was dequeued from a VOQ (in, out, prio, bytes).", + MakeTraceSourceAccessor(&P4TrafficManager::m_voqDequeueTrace), + "ns3::P4TrafficManager::QueueOpCallback") + .AddTraceSource("EgressEnqueue", + "A packet was enqueued into an egress queue (in, out, prio, bytes).", + MakeTraceSourceAccessor(&P4TrafficManager::m_egressEnqueueTrace), + "ns3::P4TrafficManager::QueueOpCallback") + .AddTraceSource("EgressDequeue", + "A packet was dequeued from an egress queue (in, out, prio, bytes).", + MakeTraceSourceAccessor(&P4TrafficManager::m_egressDequeueTrace), + "ns3::P4TrafficManager::QueueOpCallback") + .AddTraceSource("Drop", + "A packet was dropped (reason, in, out, prio, bytes).", + MakeTraceSourceAccessor(&P4TrafficManager::m_dropTrace), + "ns3::P4TrafficManager::DropCallback") + .AddTraceSource("VoqWaitingDelay", + "VOQ waiting delay of a dequeued packet.", + MakeTraceSourceAccessor(&P4TrafficManager::m_voqDelayTrace), + "ns3::P4TrafficManager::DelayCallback") + .AddTraceSource("EgressWaitingDelay", + "Egress queue waiting delay of a dequeued packet.", + MakeTraceSourceAccessor(&P4TrafficManager::m_egressDelayTrace), + "ns3::P4TrafficManager::DelayCallback") + .AddTraceSource("TotalDelay", + "Total Traffic Manager delay of a packet.", + MakeTraceSourceAccessor(&P4TrafficManager::m_totalDelayTrace), + "ns3::P4TrafficManager::DelayCallback"); + return tid; +} + +P4TrafficManager::P4TrafficManager() +{ + NS_LOG_FUNCTION(this); +} + +P4TrafficManager::~P4TrafficManager() +{ + NS_LOG_FUNCTION(this); +} + +void +P4TrafficManager::DoDispose() +{ + NS_LOG_FUNCTION(this); + m_fabricEvent.Cancel(); + for (auto& ev : m_egressEvent) + { + ev.Cancel(); + } + m_transmitCallback = nullptr; + m_voq.clear(); + m_egress.clear(); + m_voqBytes.clear(); + m_inputBufferBytes.clear(); + m_egressPortBytes.clear(); + m_egressQueueBytes.clear(); + m_egressInFlight.clear(); + m_inFlightBytes.clear(); + m_inFlightPrio.clear(); + Object::DoDispose(); +} + +void +P4TrafficManager::SetTransmitCallback(TransmitCallback cb) +{ + m_transmitCallback = std::move(cb); +} + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- + +void +P4TrafficManager::SetNumPorts(uint32_t numPorts) +{ + NS_LOG_FUNCTION(this << numPorts); + m_numPorts = numPorts; + AllocateStructures(); +} + +uint32_t +P4TrafficManager::GetNumPorts() const +{ + return m_numPorts; +} + +void +P4TrafficManager::AllocateStructures() +{ + NS_LOG_FUNCTION(this << m_numPorts); + + const uint32_t n = m_numPorts; + const size_t nn = static_cast(n) * n; + + // Reallocating cancels any in-flight scheduling from a previous sizing. + m_fabricEvent.Cancel(); + for (auto& ev : m_egressEvent) + { + ev.Cancel(); + } + m_fabricScheduled = false; + + // TmItem is move-only, so the VOQ element type cannot be relocated by + // vector::resize/assign (those instantiate a copy fallback). Direct sized + // construction value-initialises each element in place, which is fine. + m_voq = std::vector(nn); + m_egress = std::vector(n); + m_voqBytes = std::vector(nn, PriBytes{}); + m_inputBufferBytes.assign(n, 0); + m_egressPortBytes.assign(n, 0); + m_egressQueueBytes.assign(n, PriBytes{}); + m_egressBusy.assign(n, false); + m_egressEvent = std::vector(n); + m_egressInFlight.assign(n, false); + m_inFlightBytes.assign(n, 0); + m_inFlightPrio.assign(n, 0); + + m_globalBufferBytes = 0; + m_stats.perPortTxBytes.assign(n, 0); + + NS_LOG_INFO("Allocated " << (static_cast(n) * n * P4_TM_NUM_PRIORITIES) + << " VOQs for " << n << " ports"); +} + +bool +P4TrafficManager::ValidPort(uint32_t p) const +{ + return p < m_numPorts; +} + +bool +P4TrafficManager::ValidPriority(uint8_t prio) const +{ + return prio < P4_TM_NUM_PRIORITIES; +} + +// --------------------------------------------------------------------------- +// Ingress boundary: EnqueueToVoq +// --------------------------------------------------------------------------- + +bool +P4TrafficManager::EnqueueToVoq(std::unique_ptr payload, + uint32_t sizeBytes, + uint32_t inPort, + uint32_t outPort, + uint8_t priority) +{ + NS_LOG_FUNCTION(this << sizeBytes << inPort << outPort << static_cast(priority)); + + NS_ABORT_MSG_IF(!ValidPort(inPort) || !ValidPort(outPort), + "EnqueueToVoq: port index out of range (numPorts=" << m_numPorts << ")"); + NS_ABORT_MSG_IF(!ValidPriority(priority), "EnqueueToVoq: priority must be 0..7"); + + m_stats.totalReceived++; + + // Admission control, in order: global -> input -> VOQ. + auto drop = [&](TmDropReason reason) { + m_stats.totalDropped++; + m_stats.dropsByReason[static_cast(reason)]++; + m_dropTrace(static_cast(reason), inPort, outPort, priority, sizeBytes); + NS_LOG_DEBUG("Dropped " << sizeBytes << "B pkt (" << TmDropReasonToString(reason) + << ") in=" << inPort << " out=" << outPort + << " prio=" << static_cast(priority)); + // payload released on return (unique_ptr goes out of scope) + return false; + }; + + if (m_globalBufferLimit != 0 && m_globalBufferBytes + sizeBytes > m_globalBufferLimit) + { + return drop(TmDropReason::VOQ_GLOBAL_BUFFER_FULL); + } + if (m_inputBufferLimit != 0 && m_inputBufferBytes[inPort] + sizeBytes > m_inputBufferLimit) + { + return drop(TmDropReason::VOQ_INPUT_BUFFER_FULL); + } + if (m_voqLimit != 0 && m_voqBytes[Idx(inPort, outPort)][priority] + sizeBytes > m_voqLimit) + { + return drop(TmDropReason::VOQ_QUEUE_FULL); + } + + // Accept. + TmItem item; + item.payload = std::move(payload); + item.sizeBytes = sizeBytes; + item.inPort = inPort; + item.outPort = outPort; + item.priority = priority; + item.voqEnqueueTime = Simulator::Now(); + item.uid = m_nextUid++; + + m_voq[Idx(inPort, outPort)][priority].push_back(std::move(item)); + m_voqBytes[Idx(inPort, outPort)][priority] += sizeBytes; + m_inputBufferBytes[inPort] += sizeBytes; + m_globalBufferBytes += sizeBytes; + + m_stats.totalVoqEnqueued++; + m_voqEnqueueTrace(inPort, outPort, priority, sizeBytes); + + NS_LOG_DEBUG("Enqueued " << sizeBytes << "B into VOQ[" << inPort << "][" << outPort << "][" + << static_cast(priority) << "]"); + + // Event-driven mode: wake the fabric so this packet gets scheduled. + ScheduleFabricRound(); + return true; +} + +// --------------------------------------------------------------------------- +// Fabric scheduler +// --------------------------------------------------------------------------- + +std::vector +P4TrafficManager::RunFabricScheduler() +{ + return DoRunFabricScheduler(); +} + +std::vector +P4TrafficManager::DoRunFabricScheduler() +{ + NS_LOG_FUNCTION(this); + + std::vector grants; + if (m_numPorts == 0) + { + return grants; + } + + std::vector inputUsed(m_numPorts, false); + std::vector outputUsed(m_numPorts, false); + + // Priority-first maximal matching: highest priority (7) first. + for (int p = P4_TM_NUM_PRIORITIES - 1; p >= 0; --p) + { + for (uint32_t in = 0; in < m_numPorts; ++in) + { + if (inputUsed[in]) + { + continue; + } + for (uint32_t out = 0; out < m_numPorts; ++out) + { + if (outputUsed[out]) + { + continue; + } + if (VoqNotEmpty(in, out, static_cast(p))) + { + grants.push_back({in, out, static_cast(p)}); + inputUsed[in] = true; + outputUsed[out] = true; + break; // this input is now used; move to next input + } + } + } + } + + NS_LOG_DEBUG("Fabric scheduler produced " << grants.size() << " grants"); + return grants; +} + +bool +P4TrafficManager::DequeueFromVoq(uint32_t inPort, + uint32_t outPort, + uint8_t priority, + TmItem& item) +{ + NS_LOG_FUNCTION(this << inPort << outPort << static_cast(priority)); + + NS_ABORT_MSG_IF(!ValidPort(inPort) || !ValidPort(outPort) || !ValidPriority(priority), + "DequeueFromVoq: index out of range"); + + auto& q = m_voq[Idx(inPort, outPort)][priority]; + if (q.empty()) + { + return false; + } + + item = std::move(q.front()); + q.pop_front(); + + const uint32_t sizeBytes = item.sizeBytes; + m_voqBytes[Idx(inPort, outPort)][priority] -= sizeBytes; + m_inputBufferBytes[inPort] -= sizeBytes; + m_globalBufferBytes -= sizeBytes; + + m_stats.totalMovedToEgress++; + + Time voqDelay = Simulator::Now() - item.voqEnqueueTime; + m_stats.sumVoqDelay += voqDelay; + m_stats.cntVoqDelay++; + if (voqDelay > m_stats.maxQueueingDelay) + { + m_stats.maxQueueingDelay = voqDelay; + } + + m_voqDequeueTrace(inPort, outPort, priority, sizeBytes); + m_voqDelayTrace(voqDelay); + + NS_LOG_DEBUG("Dequeued " << sizeBytes << "B from VOQ[" << inPort << "][" << outPort << "][" + << static_cast(priority) << "] voqDelay=" + << voqDelay.GetNanoSeconds() << "ns"); + return true; +} + +// --------------------------------------------------------------------------- +// P3: event-driven fabric loop +// --------------------------------------------------------------------------- + +void +P4TrafficManager::ScheduleFabricRound() +{ + if (!m_eventDriven || m_fabricScheduled) + { + return; + } + m_fabricScheduled = true; + // A freshly arrived packet must traverse the ingress pipeline before it can + // be arbitrated by the fabric, so the first round after the fabric goes idle + // is delayed by the ingress pipeline delay in addition to the arbitration + // delay. (Re-arms from RunFabricRoundEvent add only the arbitration delay.) + m_fabricEvent = Simulator::Schedule(m_ingressPipelineDelay + m_fabricArbitrationDelay, + &P4TrafficManager::RunFabricRoundEvent, + this); +} + +void +P4TrafficManager::RunFabricRoundEvent() +{ + NS_LOG_FUNCTION(this); + m_fabricScheduled = false; + + std::vector grants = RunFabricScheduler(); + + // Apply grants: move each granted head packet from its VOQ to egress. The + // fabric transfers all grants of a round in parallel, so its busy time is + // the transfer time of the largest granted packet. + uint32_t maxBytes = 0; + for (const Grant& g : grants) + { + TmItem item; + if (!DequeueFromVoq(g.inPort, g.outPort, g.priority, item)) + { + continue; // defensive: VOQ emptied out from under the grant + } + maxBytes = std::max(maxBytes, item.sizeBytes); + EnqueueToEgress(std::move(item)); + } + + // Re-arm the next round while there is still demand. The next arbitration + // happens after this round's fabric transfer plus the arbitration delay. + if (AnyVoqNonEmpty()) + { + Time transfer = (maxBytes > 0) ? m_fabricRate.CalculateBytesTxTime(maxBytes) : Time(0); + m_fabricScheduled = true; + m_fabricEvent = Simulator::Schedule(m_fabricArbitrationDelay + transfer, + &P4TrafficManager::RunFabricRoundEvent, + this); + } +} + +bool +P4TrafficManager::AnyVoqNonEmpty() const +{ + for (const PriQueues& pq : m_voq) + { + for (const std::deque& q : pq) + { + if (!q.empty()) + { + return true; + } + } + } + return false; +} + +// --------------------------------------------------------------------------- +// P4: egress queues + serialising egress scheduler +// --------------------------------------------------------------------------- + +bool +P4TrafficManager::EnqueueToEgress(TmItem&& item) +{ + const uint32_t in = item.inPort; + const uint32_t out = item.outPort; + const uint8_t prio = item.priority; + const uint32_t sizeBytes = item.sizeBytes; + + auto drop = [&](TmDropReason reason) { + m_stats.totalDropped++; + m_stats.dropsByReason[static_cast(reason)]++; + m_dropTrace(static_cast(reason), in, out, prio, sizeBytes); + NS_LOG_DEBUG("Egress drop " << sizeBytes << "B (" << TmDropReasonToString(reason) + << ") out=" << out << " prio=" + << static_cast(prio)); + // The packet has already been removed from the VOQ (global was + // decremented there); dropping it here simply releases the payload. + return false; + }; + + // Admission: per-output-port then per-egress-queue. Global is NOT + // re-checked: the VOQ->egress hand-off is byte-neutral for the global + // counter (DequeueFromVoq subtracted these bytes, we add them back below), + // so the packet was already counted globally throughout its residence. + if (m_egressPortLimit != 0 && m_egressPortBytes[out] + sizeBytes > m_egressPortLimit) + { + return drop(TmDropReason::EGRESS_PORT_BUFFER_FULL); + } + if (m_egressQueueLimit != 0 && m_egressQueueBytes[out][prio] + sizeBytes > m_egressQueueLimit) + { + return drop(TmDropReason::EGRESS_QUEUE_FULL); + } + + item.egressEnqueueTime = Simulator::Now(); + m_egress[out][prio].push_back(std::move(item)); + m_egressPortBytes[out] += sizeBytes; + m_egressQueueBytes[out][prio] += sizeBytes; + m_globalBufferBytes += sizeBytes; + + m_stats.totalEgressEnqueued++; + m_egressEnqueueTrace(in, out, prio, sizeBytes); + + ScheduleEgressService(out); + return true; +} + +int +P4TrafficManager::SelectEgressPriority(uint32_t outPort) const +{ + for (int p = P4_TM_NUM_PRIORITIES - 1; p >= 0; --p) + { + if (!m_egress[outPort][static_cast(p)].empty()) + { + return p; + } + } + return -1; +} + +void +P4TrafficManager::ScheduleEgressService(uint32_t outPort) +{ + if (!m_eventDriven || m_egressBusy[outPort]) + { + return; + } + m_egressBusy[outPort] = true; + m_egressEvent[outPort] = Simulator::Schedule(m_egressPipelineDelay, + &P4TrafficManager::EgressServiceEvent, + this, + outPort); +} + +void +P4TrafficManager::EgressServiceEvent(uint32_t outPort) +{ + NS_LOG_FUNCTION(this << outPort); + + const int prio = SelectEgressPriority(outPort); + if (prio < 0) + { + m_egressBusy[outPort] = false; // nothing to send: the port goes idle + return; + } + + std::deque& q = m_egress[outPort][static_cast(prio)]; + TmItem item = std::move(q.front()); + q.pop_front(); + + const uint32_t sizeBytes = item.sizeBytes; + const uint32_t inPort = item.inPort; + + // The packet leaves the egress queue now: release its buffer occupancy and + // record its queueing delays. The "on the wire" counters (totalTransmitted + // etc.) are handled only once the frame is actually transmitted -- below in + // self-clocked mode, or on NotifyEgressTxComplete() in completion-driven + // mode. + m_egressPortBytes[outPort] -= sizeBytes; + m_egressQueueBytes[outPort][static_cast(prio)] -= sizeBytes; + m_globalBufferBytes -= sizeBytes; + + const Time now = Simulator::Now(); + const Time egressDelay = now - item.egressEnqueueTime; + const Time totalDelay = now - item.voqEnqueueTime; + + m_stats.sumEgressDelay += egressDelay; + m_stats.cntEgressDelay++; + m_stats.sumTotalDelay += totalDelay; + m_stats.cntTotalDelay++; + if (totalDelay > m_stats.maxQueueingDelay) + { + m_stats.maxQueueingDelay = totalDelay; + } + + m_egressDequeueTrace(inPort, outPort, static_cast(prio), sizeBytes); + m_egressDelayTrace(egressDelay); + m_totalDelayTrace(totalDelay); + + // Hand the frame to the datapath FIRST; it is counted as transmitted only + // after that (self-clocked below, or on the completion signal). + if (m_transmitCallback) + { + m_transmitCallback(outPort, static_cast(prio), std::move(item.payload)); + } + + if (m_egressCompletionDriven) + { + // Completion-driven: the datapath (PHY/MAC) decides when the frame has + // finished serialising and calls NotifyEgressTxComplete(), which counts + // it and serves the next frame. The port stays marked busy meanwhile. + m_egressInFlight[outPort] = true; + m_inFlightBytes[outPort] = sizeBytes; + m_inFlightPrio[outPort] = static_cast(prio); + return; + } + + // Self-clocked: count the frame as transmitted and re-arm after this port's + // serialisation time (PortRate). The next service finding the port empty is + // what finally clears m_egressBusy. + m_stats.totalTransmitted++; + m_stats.perPriorityTransmitted[static_cast(prio)]++; + m_stats.perPortTxBytes[outPort] += sizeBytes; + + const Time txTime = m_portRate.CalculateBytesTxTime(sizeBytes); + m_egressEvent[outPort] = Simulator::Schedule(txTime, + &P4TrafficManager::EgressServiceEvent, + this, + outPort); +} + +void +P4TrafficManager::NotifyEgressTxComplete(uint32_t outPort, bool success) +{ + NS_LOG_FUNCTION(this << outPort << success); + + if (!ValidPort(outPort) || !m_egressCompletionDriven || !m_egressInFlight[outPort]) + { + // Nothing in flight for this port, or not in completion-driven mode. + return; + } + + if (success) + { + const uint8_t prio = m_inFlightPrio[outPort]; + m_stats.totalTransmitted++; + m_stats.perPriorityTransmitted[prio]++; + m_stats.perPortTxBytes[outPort] += m_inFlightBytes[outPort]; + } + else + { + NS_LOG_WARN("Egress tx failed on port " << outPort << " (frame not sent)"); + } + + m_egressInFlight[outPort] = false; + + // The datapath signalled us at the moment the port became free, so serve + // the next frame now. m_egressBusy stays true across the in-flight gap; + // the next EgressServiceEvent clears it if the port has drained. + m_egressEvent[outPort] = + Simulator::ScheduleNow(&P4TrafficManager::EgressServiceEvent, this, outPort); +} + +// --------------------------------------------------------------------------- +// Occupancy queries +// --------------------------------------------------------------------------- + +size_t +P4TrafficManager::VoqLength(uint32_t inPort, uint32_t outPort, uint8_t priority) const +{ + if (!ValidPort(inPort) || !ValidPort(outPort) || !ValidPriority(priority)) + { + return 0; + } + return m_voq[Idx(inPort, outPort)][priority].size(); +} + +bool +P4TrafficManager::VoqNotEmpty(uint32_t inPort, uint32_t outPort, uint8_t priority) const +{ + return VoqLength(inPort, outPort, priority) > 0; +} + +uint64_t +P4TrafficManager::GlobalBufferBytes() const +{ + return m_globalBufferBytes; +} + +uint64_t +P4TrafficManager::InputBufferBytes(uint32_t inPort) const +{ + return ValidPort(inPort) ? m_inputBufferBytes[inPort] : 0; +} + +uint64_t +P4TrafficManager::VoqBytes(uint32_t inPort, uint32_t outPort, uint8_t priority) const +{ + if (!ValidPort(inPort) || !ValidPort(outPort) || !ValidPriority(priority)) + { + return 0; + } + return m_voqBytes[Idx(inPort, outPort)][priority]; +} + +size_t +P4TrafficManager::EgressLength(uint32_t outPort, uint8_t priority) const +{ + if (!ValidPort(outPort) || !ValidPriority(priority)) + { + return 0; + } + return m_egress[outPort][priority].size(); +} + +uint64_t +P4TrafficManager::EgressPortBytes(uint32_t outPort) const +{ + return ValidPort(outPort) ? m_egressPortBytes[outPort] : 0; +} + +uint64_t +P4TrafficManager::EgressQueueBytes(uint32_t outPort, uint8_t priority) const +{ + if (!ValidPort(outPort) || !ValidPriority(priority)) + { + return 0; + } + return m_egressQueueBytes[outPort][priority]; +} + +// --------------------------------------------------------------------------- +// Statistics +// --------------------------------------------------------------------------- + +const P4TrafficManager::TmStats& +P4TrafficManager::GetStats() const +{ + return m_stats; +} + +void +P4TrafficManager::ResetStats() +{ + const size_t nPorts = m_stats.perPortTxBytes.size(); + m_stats = TmStats{}; + m_stats.perPortTxBytes.assign(nPorts, 0); +} + +} // namespace ns3 diff --git a/utils/p4-traffic-manager.h b/utils/p4-traffic-manager.h new file mode 100644 index 0000000..d0b673b --- /dev/null +++ b/utils/p4-traffic-manager.h @@ -0,0 +1,445 @@ +/* + * Copyright (c) 2025 TU Dresden + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation; + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Authors: Vineet Goel + */ + +#ifndef P4_TRAFFIC_MANAGER_H +#define P4_TRAFFIC_MANAGER_H + +#include "ns3/data-rate.h" +#include "ns3/event-id.h" +#include "ns3/nstime.h" +#include "ns3/object.h" +#include "ns3/traced-callback.h" + +#include +#include +#include +#include +#include +#include + +namespace ns3 +{ + +/** + * \ingroup p4sim + * + * \brief High-fidelity switch Traffic Manager (VOQ + fabric arbitration). + * + * This is the first stage of a high-fidelity traffic-manager architecture that + * replaces the output-only priority scheduler (NSQueueingLogicPriRL). The + * intended packet flow is: + * + * ingress pipeline -> EnqueueToVoq(VOQ[in][out][prio]) + * -> fabric scheduler grants (in -> out) + * -> egress queue (per output port / priority) + * -> egress pipeline -> ns-3 NetDevice + * + * This file implements phases P1 (VOQ + finite-buffer accounting + drop + * reasons + stats/traces), P2 (priority-first maximal-matching fabric + * scheduler), P3 (ns-3 event-driven fabric-round timing) and P4 (per-output + * strict-priority egress queues + serialising egress scheduler). + * + * Two ways to drive the pipeline: + * - Manual (default): call RunFabricScheduler()/DequeueFromVoq() yourself. + * Nothing is scheduled on the ns-3 event queue. Used by the low-level + * unit tests. + * - Event-driven (attribute "EventDriven" = true): EnqueueToVoq() arms a + * self-clocking fabric loop (Simulator::Schedule, no threads) that moves + * packets VOQ -> egress -> wire, respecting fabric/port rates and the + * arbitration/pipeline delays. Transmitted packets are handed to the + * TransmitCallback (wired to an ns-3 NetDevice at integration time). + * + * Packet-format boundary: the Traffic Manager does NOT assume ns3::Packet. It + * carries an opaque, move-only ::ns3::TmPayload (see BmPacketPayload for the + * bm::Packet wrapper used by the real switch core). Scheduling and accounting + * are driven solely by the metadata in TmItem, never by the payload contents. + */ + +/// Number of priority levels. The priority field is 3 bits, so 8 levels. +/// Higher value = higher priority (priority 7 is the highest). +static constexpr uint8_t P4_TM_NUM_PRIORITIES = 8; + +/** + * \brief Reason a packet was dropped by the Traffic Manager. + */ +enum class TmDropReason : uint8_t +{ + VOQ_GLOBAL_BUFFER_FULL = 0, ///< global buffer would overflow + VOQ_INPUT_BUFFER_FULL, ///< per-input-port buffer would overflow + VOQ_QUEUE_FULL, ///< the target VOQ[in][out][prio] would overflow + EGRESS_PORT_BUFFER_FULL, ///< per-output-port egress buffer would overflow + EGRESS_QUEUE_FULL, ///< the target egress queue[out][prio] would overflow +}; + +/// Human-readable name for a drop reason (for logging/tracing). +const char* TmDropReasonToString(TmDropReason r); + +/** + * \brief Opaque, move-only payload carried by the Traffic Manager. + * + * The TM never inspects the payload; it only moves it from VOQ to egress. + * Concrete payloads (a bm::Packet wrapper, or a unit-test stub) derive from + * this base. This keeps the TM decoupled from any specific packet format. + */ +class TmPayload +{ + public: + virtual ~TmPayload() = default; +}; + +/** + * \brief A unit of work inside the Traffic Manager. + * + * Packet-level only: no cell segmentation. Move-only, because it owns the + * payload. + */ +struct TmItem +{ + std::unique_ptr payload; ///< opaque payload (bm::Packet, test stub, ...) + uint32_t sizeBytes{0}; ///< size in bytes, used for buffer accounting + uint32_t inPort{0}; ///< ingress port + uint32_t outPort{0}; ///< egress port chosen by ingress + uint8_t priority{0}; ///< 0..7, 7 = highest + Time voqEnqueueTime; ///< when the item entered the VOQ + Time egressEnqueueTime; ///< when the item entered the egress queue + uint64_t uid{0}; ///< monotonically increasing id (tracing / tie-break) + + TmItem() = default; + TmItem(TmItem&&) = default; + TmItem& operator=(TmItem&&) = default; + TmItem(const TmItem&) = delete; + TmItem& operator=(const TmItem&) = delete; +}; + +/** + * \brief A fabric matching decision produced by the scheduler. + * + * Instructs the fabric to move one packet from VOQ[inPort][outPort][priority] + * to the egress side. + */ +struct Grant +{ + uint32_t inPort; + uint32_t outPort; + uint8_t priority; +}; + +/** + * \ingroup p4sim + * \brief Traffic Manager: input-side VOQ + priority-first fabric scheduler. + */ +class P4TrafficManager : public Object +{ + public: + /// TracedCallback signature for VOQ/egress enqueue and dequeue events. + typedef void (*QueueOpCallback)(uint32_t inPort, + uint32_t outPort, + uint8_t priority, + uint32_t sizeBytes); + /// TracedCallback signature for drop events. + typedef void (*DropCallback)(uint8_t reason, + uint32_t inPort, + uint32_t outPort, + uint8_t priority, + uint32_t sizeBytes); + /// TracedCallback signature for waiting-delay measurements. + typedef void (*DelayCallback)(Time delay); + + /** + * \brief Delivery hook invoked when the egress scheduler serialises a + * packet onto the wire (event-driven mode only). + * + * The Traffic Manager transfers ownership of the payload to the callback. + * At integration time this is wired to the ns-3 NetDevice send path; the + * unit tests use it to observe transmit order. std::function (not + * ns3::Callback) because it carries a move-only unique_ptr argument. + */ + using TransmitCallback = + std::function payload)>; + + static TypeId GetTypeId(); + + P4TrafficManager(); + ~P4TrafficManager() override; + + // ---- Setup ---- + + /** + * \brief Set the number of switch ports and (re)allocate all structures. + * + * Allocates N*N*8 VOQs and the associated byte counters. Also settable + * via the "NumPorts" attribute. + * \param numPorts number of ports N. + */ + void SetNumPorts(uint32_t numPorts); + uint32_t GetNumPorts() const; + + /** + * \brief Set the delivery hook for transmitted packets (event-driven mode). + * \param cb callback receiving (outPort, priority, payload). + */ + void SetTransmitCallback(TransmitCallback cb); + + /** + * \brief Datapath -> Traffic Manager transmit-completion signal. + * + * In completion-driven egress mode (attribute "EgressCompletionDriven"), + * EgressServiceEvent() hands each frame to the TransmitCallback and then + * waits: it is the datapath (PHY/MAC) that decides when the frame has + * finished serialising onto the wire and calls this to report it. Only on + * that signal does the Traffic Manager count the frame as transmitted and + * release the port to serve the next frame. This decouples "which packet + * goes next" (the TM's decision) from "when the wire is free" (the PHY's). + * + * No-op unless the port has a frame in flight in completion-driven mode. + * + * \param outPort output port whose in-flight frame just finished. + * \param success true if the frame was accepted onto the wire; false if + * the datapath could not send it (counted as a tx failure). + */ + void NotifyEgressTxComplete(uint32_t outPort, bool success); + + // ---- Ingress boundary ---- + + /** + * \brief Enqueue a packet into VOQ[inPort][outPort][priority]. + * + * Finite-buffer admission is checked in order: global, input, VOQ. If any + * limit would be exceeded the packet is dropped, m_dropTrace fires with the + * corresponding TmDropReason, and the function returns false (payload is + * released). + * + * \param payload the opaque payload (ownership taken on success). + * \param sizeBytes packet size in bytes (for accounting). + * \param inPort ingress port (< numPorts). + * \param outPort egress port (< numPorts). + * \param priority priority 0..7 (7 = highest). + * \return true if accepted into the VOQ, false if dropped. + */ + bool EnqueueToVoq(std::unique_ptr payload, + uint32_t sizeBytes, + uint32_t inPort, + uint32_t outPort, + uint8_t priority); + + // ---- Fabric ---- + + /** + * \brief Run one round of fabric matching. + * + * Delegates to DoRunFabricScheduler() (overridable to swap in iSLIP / + * round-robin). The default policy is priority-first maximal matching. + * \return the list of grants for this round. + */ + std::vector RunFabricScheduler(); + + /** + * \brief Remove the head item of VOQ[inPort][outPort][priority]. + * + * Used to apply a Grant. Updates all byte counters and fires the VOQ + * dequeue / VOQ-waiting-delay traces. + * \param inPort ingress port. + * \param outPort egress port. + * \param priority priority level. + * \param[out] item receives the dequeued item on success. + * \return true if an item was dequeued, false if the VOQ was empty. + */ + bool DequeueFromVoq(uint32_t inPort, uint32_t outPort, uint8_t priority, TmItem& item); + + // ---- Occupancy queries ---- + + /// Number of packets queued in VOQ[in][out][prio]. + size_t VoqLength(uint32_t inPort, uint32_t outPort, uint8_t priority) const; + /// True if VOQ[in][out][prio] holds at least one packet. + bool VoqNotEmpty(uint32_t inPort, uint32_t outPort, uint8_t priority) const; + /// Total bytes occupied across VOQ (and, later, egress). + uint64_t GlobalBufferBytes() const; + /// Bytes occupied by all VOQs of a given input port. + uint64_t InputBufferBytes(uint32_t inPort) const; + /// Bytes occupied by VOQ[in][out][prio]. + uint64_t VoqBytes(uint32_t inPort, uint32_t outPort, uint8_t priority) const; + + /// Number of packets queued in egress[out][prio]. + size_t EgressLength(uint32_t outPort, uint8_t priority) const; + /// Bytes occupied by all egress queues of a given output port. + uint64_t EgressPortBytes(uint32_t outPort) const; + /// Bytes occupied by egress[out][prio]. + uint64_t EgressQueueBytes(uint32_t outPort, uint8_t priority) const; + + // ---- Statistics ---- + + /** + * \brief Cumulative Traffic Manager statistics. + */ + struct TmStats + { + uint64_t totalReceived{0}; ///< packets offered to EnqueueToVoq + uint64_t totalVoqEnqueued{0}; ///< packets accepted into a VOQ + uint64_t totalMovedToEgress{0}; ///< packets dequeued from VOQ (granted) + uint64_t totalEgressEnqueued{0}; ///< packets accepted into an egress queue + uint64_t totalTransmitted{0}; ///< packets serialised onto the wire + uint64_t totalDropped{0}; ///< packets dropped for any reason + std::array dropsByReason{}; ///< indexed by TmDropReason + std::array perPriorityTransmitted{}; + + // delay accumulators (sums + counts -> averages via helpers) + Time sumVoqDelay; + uint64_t cntVoqDelay{0}; + Time sumEgressDelay; + uint64_t cntEgressDelay{0}; + Time sumTotalDelay; + uint64_t cntTotalDelay{0}; + Time maxQueueingDelay; + + std::vector perPortTxBytes; ///< bytes transmitted per output port + + Time AvgVoqDelay() const; + Time AvgEgressDelay() const; + Time AvgTotalDelay() const; + }; + + const TmStats& GetStats() const; + void ResetStats(); + + protected: + void DoDispose() override; + + /** + * \brief The fabric matching policy. + * + * Default: priority-first maximal matching. For priority 7 down to 0, for + * each unused input port, grant the first unused output port that has a + * non-empty VOQ at that priority. One input grants at most one packet and + * one output receives at most one packet per round. + * + * Override this method (or replace the whole object) to implement iSLIP, + * round-robin, etc. It only needs VoqNotEmpty() as its primitive. + * \return the grants for this round. + */ + virtual std::vector DoRunFabricScheduler(); + + private: + /// (Re)allocate VOQ and accounting structures for m_numPorts ports. + void AllocateStructures(); + /// True if idx < m_numPorts and priority < 8. + bool ValidPort(uint32_t p) const; + bool ValidPriority(uint8_t prio) const; + /// Flatten (inPort, outPort) into the [in*N + out] VOQ index. + size_t Idx(uint32_t inPort, uint32_t outPort) const + { + return static_cast(inPort) * m_numPorts + outPort; + } + + // ---- P3: event-driven fabric loop ---- + /// Arm a fabric round if one is not already pending (event-driven mode). + void ScheduleFabricRound(); + /// One fabric round: apply grants (VOQ -> egress), then re-arm if work remains. + void RunFabricRoundEvent(); + /// True if any VOQ holds at least one packet. + bool AnyVoqNonEmpty() const; + + // ---- P4: egress side ---- + /// Move a granted item into egress[out][prio]; false (and dropped) if full. + bool EnqueueToEgress(TmItem&& item); + /// Arm the serialising egress scheduler for one output port, if idle. + void ScheduleEgressService(uint32_t outPort); + /// Serialise the highest-priority egress packet of a port, then re-arm. + void EgressServiceEvent(uint32_t outPort); + /// Highest non-empty egress priority of a port, or -1 if all empty. + int SelectEgressPriority(uint32_t outPort) const; + + uint32_t m_numPorts{0}; + + /// Event-driven mode flag (attribute "EventDriven"); see class doc. + bool m_eventDriven{false}; + + /// Completion-driven egress (attribute "EgressCompletionDriven"). When + /// true, the egress scheduler hands each frame to the TransmitCallback and + /// waits for NotifyEgressTxComplete() before counting it as transmitted and + /// serving the next frame (the PHY decides the timing). When false + /// (default), egress self-clocks its serialisation at PortRate. Only + /// meaningful together with EventDriven. + bool m_egressCompletionDriven{false}; + + // Finite-buffer limits (bytes). 0 means "no limit". + uint64_t m_globalBufferLimit{0}; + uint64_t m_inputBufferLimit{0}; + uint64_t m_voqLimit{0}; + uint64_t m_egressPortLimit{0}; + uint64_t m_egressQueueLimit{0}; + + // Timing configuration (used by later phases; part of the config surface). + DataRate m_portRate; + DataRate m_fabricRate; + Time m_ingressPipelineDelay; + Time m_fabricArbitrationDelay; + Time m_egressPipelineDelay; + + // ---- VOQ storage: logically queues[inPort][outPort][priority] ---- + // Stored flat (size N*N, indexed by Idx(in,out)) because the element type + // holds move-only TmItem and cannot be relocated by vector::resize/assign. + using PriQueues = std::array, P4_TM_NUM_PRIORITIES>; + std::vector m_voq; ///< [Idx(in,out)] -> 8 priority deques + + // ---- Egress storage: per output port, 8 strict-priority queues ---- + std::vector m_egress; ///< [out] -> 8 priority deques + + // ---- Byte accounting ---- + uint64_t m_globalBufferBytes{0}; + std::vector m_inputBufferBytes; ///< [in] + using PriBytes = std::array; + std::vector m_voqBytes; ///< [Idx(in,out)] -> 8 counters + std::vector m_egressPortBytes; ///< [out] + std::vector m_egressQueueBytes; ///< [out] -> 8 counters + + // ---- Event-driven scheduling state ---- + bool m_fabricScheduled{false}; ///< a fabric round is pending + EventId m_fabricEvent; ///< pending fabric-round event + std::vector m_egressBusy; ///< [out] port is serialising + std::vector m_egressEvent; ///< [out] pending egress-service event + TransmitCallback m_transmitCallback; ///< delivery hook (set by integration) + + // Completion-driven egress: the frame currently handed to the datapath and + // awaiting a NotifyEgressTxComplete() for that output port. + std::vector m_egressInFlight; ///< [out] a frame is on the wire + std::vector m_inFlightBytes; ///< [out] size of the in-flight frame + std::vector m_inFlightPrio; ///< [out] priority of the in-flight frame + + uint64_t m_nextUid{0}; + TmStats m_stats; + + // ---- Trace sources ---- + TracedCallback m_voqEnqueueTrace; + TracedCallback m_voqDequeueTrace; + TracedCallback m_egressEnqueueTrace; + TracedCallback m_egressDequeueTrace; + TracedCallback m_dropTrace; + TracedCallback