diff --git a/src/inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime.msg b/src/inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime.msg new file mode 100644 index 00000000000..cbbca1fda35 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime.msg @@ -0,0 +1,24 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +import inet.common.INETDefs; +import inet.linklayer.common.MacAddress; + +namespace inet::ieee80211; + +// +// Carries the on-air time actually consumed by a transmitted frame, together with +// the receiver it was sent to. Emitted by the coordination function (~Dcf, ~Hcf) +// as the object value of the `frameTransmittedAirtime` signal, once per on-air +// transmission (so retries are counted), and consumed by ~AirtimeFairnessQueue to +// charge the receiver's airtime deficit a-posteriori. +// +class Ieee80211TransmittedAirtime extends cObject +{ + MacAddress receiverAddress; + simtime_t airtime; +} diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 0da41104ace..4841707ec32 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -9,6 +9,7 @@ #include "inet/common/ModuleAccess.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime_m.h" #include "inet/linklayer/ieee80211/mac/framesequence/DcfFs.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" @@ -18,6 +19,8 @@ namespace ieee80211 { using namespace inet::physicallayer; +simsignal_t Dcf::frameTransmittedAirtimeSignal = cComponent::registerSignal("frameTransmittedAirtime"); + Define_Module(Dcf); void Dcf::initialize(int stage) @@ -239,6 +242,20 @@ void Dcf::transmissionComplete(Packet *packet, const PtrisSequenceRunning()) { + // Account the on-air time of the just-transmitted unicast data/mgmt frame to its + // receiver, a-posteriori (so each retransmission is charged), for airtime-fair + // transmit scheduling in AirtimeFairnessQueue. Control frames and multicast are + // excluded; the duration is exact (computed from the selected mode and length). + if (auto dataOrMgmtHeader = dynamicPtrCast(header)) { + auto receiver = dataOrMgmtHeader->getReceiverAddress(); + if (!receiver.isMulticast()) { + auto mode = rateSelection->computeMode(packet, header); + Ieee80211TransmittedAirtime info; + info.setReceiverAddress(receiver); + info.setAirtime(mode->getDuration(packet->getDataLength())); + emit(frameTransmittedAirtimeSignal, &info); + } + } frameSequenceHandler->transmissionComplete(); } else diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index a4fb17cb284..1d455d8fccc 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -39,6 +39,9 @@ class Ieee80211Mac; */ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public ModeSetListener { + public: + static simsignal_t frameTransmittedAirtimeSignal; + protected: Ieee80211Mac *mac = nullptr; IRateControl *dataAndMgmtRateControl = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.ned index eeab697b3a2..3f0dae6640b 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.ned @@ -44,6 +44,7 @@ module Dcf extends Module like IDcf @signal[frameSequenceStarted]; @signal[frameSequenceFinished]; @signal[datarateSelected](type=double); + @signal[frameTransmittedAirtime](type=inet::ieee80211::Ieee80211TransmittedAirtime); // receiver + on-air time, once per transmitted frame @statistic[packetSentToPeer](title="packets sent"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerUnicast](title="packets sent: unicast"; source=ieee80211Unicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerMulticast](title="packets sent: multicast"; source=ieee80211Multicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index b3d11467343..be55a103dfc 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -12,6 +12,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime_m.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" @@ -24,6 +25,7 @@ using namespace inet::physicallayer; simsignal_t Hcf::edcaCollisionDetectedSignal = cComponent::registerSignal("edcaCollisionDetected"); simsignal_t Hcf::blockAckAgreementAddedSignal = cComponent::registerSignal("blockAckAgreementAdded"); simsignal_t Hcf::blockAckAgreementDeletedSignal = cComponent::registerSignal("blockAckAgreementDeleted"); +simsignal_t Hcf::frameTransmittedAirtimeSignal = cComponent::registerSignal("frameTransmittedAirtime"); Define_Module(Hcf); @@ -369,6 +371,20 @@ void Hcf::transmissionComplete(Packet *packet, const PtrgetChannelOwner(); if (edcaf) { + // Account the on-air time of the just-transmitted unicast data/mgmt frame to its + // receiver, a-posteriori (so each retransmission is charged), for airtime-fair + // transmit scheduling in AirtimeFairnessQueue. Control frames and multicast are + // excluded; the duration is exact (computed from the selected mode and length). + if (auto dataOrMgmtHeader = dynamicPtrCast(header)) { + auto receiver = dataOrMgmtHeader->getReceiverAddress(); + if (!receiver.isMulticast()) { + auto mode = rateSelection->computeMode(packet, header, edcaf->getTxopProcedure()); + Ieee80211TransmittedAirtime info; + info.setReceiverAddress(receiver); + info.setAirtime(mode->getDuration(packet->getDataLength())); + emit(frameTransmittedAirtimeSignal, &info); + } + } frameSequenceHandler->transmissionComplete(); } else if (hcca->isOwning()) diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 2060a4e3d2c..7804b714f9f 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -51,6 +51,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: static simsignal_t edcaCollisionDetectedSignal; static simsignal_t blockAckAgreementAddedSignal; static simsignal_t blockAckAgreementDeletedSignal; + static simsignal_t frameTransmittedAirtimeSignal; protected: Ieee80211Mac *mac = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index 165edc93513..6cb51e9ac13 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -51,6 +51,7 @@ module Hcf extends Module like IHcf @signal[datarateSelected](type=double); @signal[blockAckAgreementAdded]; @signal[blockAckAgreementDeleted]; + @signal[frameTransmittedAirtime](type=inet::ieee80211::Ieee80211TransmittedAirtime); // receiver + on-air time, once per transmitted frame @statistic[packetSentToPeer](title="packets sent"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerUnicast](title="packets sent: unicast"; source=ieee80211Unicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerMulticast](title="packets sent: multicast"; source=ieee80211Multicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessCompoundQueue.cc b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessCompoundQueue.cc new file mode 100644 index 00000000000..3d5120be992 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessCompoundQueue.cc @@ -0,0 +1,30 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include "inet/linklayer/ieee80211/mac/queue/AirtimeFairnessCompoundQueue.h" + +#include "inet/common/InitStages.h" + +namespace inet { +namespace ieee80211 { + +Define_Module(AirtimeFairnessCompoundQueue); + +void AirtimeFairnessCompoundQueue::initialize(int stage) +{ + CompoundPacketQueueBase::initialize(stage); + if (stage == INITSTAGE_LOCAL) { + // Number of per-station branches created so far (= stations served). Watched on this + // module so the display string can use {numStations}; a cross-submodule reference like + // {classifier.numStations} would make ModuleMixin call getModuleByPath(), which throws + // in OMNeT++ 6 and breaks every Qtenv refreshDisplay (Cmdenv never evaluates it). + WATCH_EXPR("numStations", (int)getSubmoduleVectorSize("branch")); + } +} + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessCompoundQueue.h b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessCompoundQueue.h new file mode 100644 index 00000000000..2d2156bc575 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessCompoundQueue.h @@ -0,0 +1,35 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_AIRTIMEFAIRNESSCOMPOUNDQUEUE_H +#define __INET_AIRTIMEFAIRNESSCOMPOUNDQUEUE_H + +#include "inet/queueing/queue/CompoundPacketQueueBase.h" + +namespace inet { +namespace ieee80211 { + +/** + * The compound-module class behind the airtime-fairness compound queue. It only publishes the + * number of per-station branches created so far as a `numStations` watch, so the queue's + * display string can refer to it as {numStations}. + * + * See the corresponding NED file for the submodule structure. + * + * @see DynamicClassifier, AirtimeFairnessGate, AirtimeFairnessScheduler, + * Ieee80211LongestFlowDropper + */ +class INET_API AirtimeFairnessCompoundQueue : public queueing::CompoundPacketQueueBase +{ + protected: + virtual void initialize(int stage) override; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.cc b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.cc new file mode 100644 index 00000000000..cad86798bbb --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.cc @@ -0,0 +1,145 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include "inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.h" + +#include "inet/common/ModuleAccess.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime_m.h" +#include "inet/networklayer/common/NetworkInterface.h" + +namespace inet { +namespace ieee80211 { + +Define_Module(AirtimeFairnessGate); + +simsignal_t AirtimeFairnessGate::deficitChangedSignal = cComponent::registerSignal("deficitChanged"); + +void AirtimeFairnessGate::initialize(int stage) +{ + PacketGateBase::initialize(stage); + if (stage == INITSTAGE_LOCAL) { + quantum = par("quantum"); + weight = par("weight"); + fairnessEnabled = par("fairnessEnabled"); + if (quantum <= SIMTIME_ZERO) + throw cRuntimeError("The quantum parameter must be positive"); + if (weight <= 0) + throw cRuntimeError("The weight parameter must be positive"); + frameTransmittedAirtimeSignal = registerSignal("frameTransmittedAirtime"); + isOpen_ = isEligible(); // deficit starts at zero -> eligible -> open + WATCH(stationAddress); + WATCH(deficit); + } + else if (stage == INITSTAGE_LINK_LAYER) + // The coordination function (Dcf/Hcf) emits frameTransmittedAirtime from within the + // containing network interface; subscribing there scopes the accounting to this + // interface's transmissions (the frame's receiver disambiguates the stations). + getContainingNicModule(this)->subscribe(frameTransmittedAirtimeSignal, this); +} + +void AirtimeFairnessGate::processPacket(Packet *packet) +{ + PacketGateBase::processPacket(packet); + if (stationAddress.isUnspecified()) { + const auto& header = packet->peekAtFront(); + stationAddress = header->getReceiverAddress(); + } +} + +void AirtimeFairnessGate::setDeficit(simtime_t value) +{ + if (deficit != value) { + deficit = value; + emit(deficitChangedSignal, deficit.dbl()); + } +} + +void AirtimeFairnessGate::addQuantum() +{ + Enter_Method("addQuantum"); + setDeficit(deficit + quantum * weight); + updateGateState(); +} + +void AirtimeFairnessGate::updateGateState() +{ + bool eligible = isEligible(); + if (eligible && isClosed()) + open(); + else if (!eligible && isOpen()) + close(); +} + +bool AirtimeFairnessGate::isBacklogged() const +{ + return provider != nullptr && provider.canPullSomePacket(); +} + +Packet *AirtimeFairnessGate::peekPacket() const +{ + return provider != nullptr ? provider.canPullPacket() : nullptr; +} + +void AirtimeFairnessGate::handleCanPullPacketChanged(const cGate *gate) +{ + Enter_Method("handleCanPullPacketChanged"); + // Forward even while the gate is closed: a station can become backlogged while its + // deficit is negative (gate shut), and the scheduler must still learn about it so it + // can top the station up and eventually serve it. PacketGateBase would swallow this + // notification while the gate is closed. + if (collector != nullptr) + collector.handleCanPullPacketChanged(); +} + +int AirtimeFairnessGate::getNumPackets() const +{ + return queueing::PacketFlowBase::getNumPackets(); +} + +b AirtimeFairnessGate::getTotalLength() const +{ + return queueing::PacketFlowBase::getTotalLength(); +} + +Packet *AirtimeFairnessGate::getPacket(int index) const +{ + return queueing::PacketFlowBase::getPacket(index); +} + +bool AirtimeFairnessGate::isEmpty() const +{ + return queueing::PacketFlowBase::isEmpty(); +} + +void AirtimeFairnessGate::removePacket(Packet *packet) +{ + queueing::PacketFlowBase::removePacket(packet); +} + +void AirtimeFairnessGate::removeAllPackets() +{ + queueing::PacketFlowBase::removeAllPackets(); +} + +void AirtimeFairnessGate::receiveSignal(cComponent *source, simsignal_t signalID, cObject *object, cObject *details) +{ + if (signalID == frameTransmittedAirtimeSignal) { + Enter_Method("%s", cComponent::getSignalName(signalID)); + auto info = check_and_cast(object); + // charge only frames sent to the station this gate serves + if (!stationAddress.isUnspecified() && info->getReceiverAddress() == stationAddress) { + setDeficit(deficit - info->getAirtime()); + EV_DEBUG << "Charged " << info->getAirtime() << " airtime to " << stationAddress + << ", deficit now " << deficit << EV_ENDL; + updateGateState(); // may close the gate if the deficit went negative + } + } +} + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.h b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.h new file mode 100644 index 00000000000..9a7d6e6b31c --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.h @@ -0,0 +1,121 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_AIRTIMEFAIRNESSGATE_H +#define __INET_AIRTIMEFAIRNESSGATE_H + +#include "inet/linklayer/common/MacAddress.h" +#include "inet/queueing/base/PacketGateBase.h" + +namespace inet { +namespace ieee80211 { + +/** + * A per-station airtime-fairness gate for the ~AirtimeFairnessQueue compound queue. + * One gate sits on the transmit path of exactly one receiver (destination station); + * it holds that station's airtime `deficit` and is *open* while the station is + * eligible to transmit (deficit >= 0) and *closed* otherwise. This mirrors the way a + * ~PeriodicGate gates one sub-queue in a ~GatingPriorityQueue, except that eligibility + * is driven by on-air time rather than by a schedule. + * + * The deficit is charged a-posteriori from the actual on-air duration of each + * transmitted frame (retries included), reported by the coordination function + * (~Dcf/~Hcf) via the `frameTransmittedAirtime` signal (see ~Ieee80211TransmittedAirtime). The + * gate learns which station it serves from the receiver address of the first frame that + * passes through it, so that later airtime charges can be matched to it. + * + * The gate forms a matched pair with ~AirtimeFairnessScheduler: the scheduler owns the + * deficit round-robin rotation and tops up an ineligible-but-backlogged gate by one + * `quantum * weight` when its turn comes (see addQuantum()); the gate owns the deficit + * state and the open/closed eligibility. Because a closed gate hides its backlog from + * the generic pull interface, the gate exposes isBacklogged()/peekPacket() for the + * scheduler and forwards backlog-change notifications even while closed. + * + * With `fairnessEnabled = false` the gate stays permanently open, degrading the compound + * queue to a plain per-station round robin (the frame-fair anomaly baseline). + * + * See the corresponding NED file for more details. + * + * @see AirtimeFairnessQueue, AirtimeFairnessScheduler, Ieee80211TransmittedAirtime + */ +class INET_API AirtimeFairnessGate : public queueing::PacketGateBase, public cListener +{ + public: + static simsignal_t deficitChangedSignal; + + protected: + // parameters + simtime_t quantum; // airtime top-up granted per round-robin turn (scaled by weight) + double weight = 1.0; // per-station airtime weight; equal weight means equal airtime share + bool fairnessEnabled = true; // when false the gate stays open (plain round robin) + + // state + MacAddress stationAddress; // receiver this gate serves; learned from the first frame passing through + simtime_t deficit = SIMTIME_ZERO; // current airtime deficit; the gate is open while this is non-negative + + simsignal_t frameTransmittedAirtimeSignal = SIMSIGNAL_NULL; + + protected: + virtual void initialize(int stage) override; + virtual void processPacket(Packet *packet) override; + + virtual bool isEligible() const { return !fairnessEnabled || deficit >= SIMTIME_ZERO; } + virtual void updateGateState(); + virtual void setDeficit(simtime_t value); + + public: + /** @name Matched-pair interface used by ~AirtimeFairnessScheduler */ + //@{ + /** + * Returns the station's current airtime deficit; a non-negative value means the + * station is eligible to transmit. + */ + virtual simtime_t getDeficit() const { return deficit; } + /** + * Grants one round-robin quantum (`quantum * weight`) of airtime credit and reopens + * the gate if the deficit thereby became non-negative. + */ + virtual void addQuantum(); + /** + * Returns whether the upstream sub-queue holds a frame, regardless of whether this + * gate is currently open or closed. + */ + virtual bool isBacklogged() const; + /** + * Returns the head frame of the upstream sub-queue (regardless of open/closed), or + * nullptr if the sub-queue is empty. + */ + virtual Packet *peekPacket() const; + /** + * Returns the receiver address this gate serves, or an unspecified address until the + * first frame has passed through. + */ + virtual const MacAddress& getStationAddress() const { return stationAddress; } + //@} + + virtual void handleCanPullPacketChanged(const cGate *gate) override; + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *object, cObject *details) override; + + /** @name IPacketCollection: report the true upstream backlog even while the gate is closed */ + //@{ + // PacketGateBase hides the backlog (reports 0) while closed; here the gate is only an + // airtime-eligibility valve, so the enclosing compound queue must still see the frames + // buffered behind it for correct queue-length and capacity accounting. + virtual int getNumPackets() const override; + virtual b getTotalLength() const override; + virtual Packet *getPacket(int index) const override; + virtual bool isEmpty() const override; + virtual void removePacket(Packet *packet) override; + virtual void removeAllPackets() override; + //@} +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.ned b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.ned new file mode 100644 index 00000000000..72c48ac9a93 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.ned @@ -0,0 +1,47 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.linklayer.ieee80211.mac.queue; + +import inet.queueing.base.PacketGateBase; +import inet.queueing.contract.IPacketGate; + +// +// A per-station airtime-fairness gate for the ~AirtimeFairnessQueue compound queue. One +// gate sits on the transmit path of exactly one receiver (destination station); it holds +// that station's airtime deficit and is open while the station is eligible to transmit +// (deficit >= 0) and closed otherwise. This mirrors the way a ~PeriodicGate gates one +// sub-queue in a ~GatingPriorityQueue, except that eligibility is driven by on-air time +// rather than by a schedule. +// +// The deficit is charged a-posteriori from the actual on-air duration of each transmitted +// frame (retries included), reported by the coordination function (~Dcf/~Hcf) via the +// `frameTransmittedAirtime` signal (see ~Ieee80211TransmittedAirtime). The gate learns which +// station it serves from the receiver address of the first frame that passes through it. +// +// The gate forms a matched pair with ~AirtimeFairnessScheduler: the scheduler owns the +// deficit round-robin rotation and tops up an ineligible-but-backlogged gate by one +// `quantum * weight` when its turn comes; the gate owns the deficit state and the +// open/closed eligibility. +// +// With `fairnessEnabled = false` the gate stays permanently open, degrading the compound +// queue to a plain per-station round robin (the frame-fair anomaly baseline). +// +// @see ~AirtimeFairnessQueue, ~AirtimeFairnessScheduler, ~DynamicClassifier +// +simple AirtimeFairnessGate extends PacketGateBase like IPacketGate +{ + parameters: + double quantum @unit(s) = default(1500us); // airtime top-up granted per round-robin turn (scaled by weight); ~one max-length frame's airtime + double weight = default(1); // per-station airtime weight; equal weight means equal airtime share + bool fairnessEnabled = default(true); // when false the gate stays open (plain per-station round robin) + displayStringTextFormat = default("deficit {deficit}\npassed %p pk (%l)"); + @class(AirtimeFairnessGate); + @signal[deficitChanged](type=double); + // the statistical value is the current airtime deficit of the served station + @statistic[deficit](title="airtime deficit"; source=deficitChanged; record=vector; unit=s; interpolationmode=sample-hold); +} diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessQueue.ned b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessQueue.ned new file mode 100644 index 00000000000..2976680f945 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessQueue.ned @@ -0,0 +1,83 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.linklayer.ieee80211.mac.queue; + +import inet.queueing.classifier.DynamicClassifier; +import inet.queueing.contract.IPacketQueue; +import inet.queueing.queue.CompoundPacketQueueBase; + +// +// A per-station airtime deficit round-robin (DRR) transmit queue for IEEE 802.11, modelled +// on the Linux `mac80211` airtime-fairness feature. It is built as a compound queue: a +// classifier routes each frame to a per-receiver sub-queue, each sub-queue is followed by a +// per-station airtime gate, and a deficit-round-robin scheduler serves the gates so that, +// over time, every backlogged station gets an equal share of on-air *time* (scaled by an +// optional per-station weight) rather than an equal share of frames. +// +// The per-station branches (sub-queue + gate) are created on demand as new receivers +// appear, so the queue adapts to the dynamic set of stations an access point serves. The +// sub-queue type is configurable via `subqueueTypename`. +// +// This fixes the downlink form of the 802.11 rate anomaly: when an access point saturates a +// mix of fast and slow clients, a plain FIFO (or frame-fair round robin) lets the slow +// client's long frames drag the fast clients down to its throughput; airtime fairness holds +// the slow client to its fair *time* share and lets the fast clients recover. +// +// Each station's airtime deficit is charged a-posteriori from the actual on-air duration of +// its transmitted frames (retries included), reported by the coordination function via the +// `frameTransmittedAirtime` signal. A shared `packetCapacity` is enforced across all +// stations with a drop-from-longest overflow policy (see ~Ieee80211LongestFlowDropper), so a +// slow station's backlog cannot lock the others out. With `fairnessEnabled = false` the +// queue degrades to a plain per-station round robin (the frame-fair anomaly baseline). +// +// Drops into the `pendingQueue` slot of an ~Edcaf or ~Dcaf via a typename override, e.g.: +// *.host.wlan[*].mac.dcf.channelAccess.pendingQueue.typename = "AirtimeFairnessQueue" +// +// @see ~DynamicClassifier, ~AirtimeFairnessGate, ~AirtimeFairnessScheduler, +// ~Ieee80211LongestFlowDropper, ~PendingQueue +// +module AirtimeFairnessQueue extends CompoundPacketQueueBase +{ + parameters: + @class(AirtimeFairnessCompoundQueue); + double quantum @unit(s) = default(1500us); // base DRR quantum in airtime units; ~one max-length frame's airtime + double weight = default(1); // per-station airtime weight; equal weight means equal airtime share + bool fairnessEnabled = default(true); // when false, degrades to a plain per-station round robin (for OFF/ON contrast) + string subqueueTypename = default("inet.queueing.queue.PacketQueue"); // NED type of each per-station sub-queue + packetCapacity = default(100); // shared across all per-station sub-queues; same limit as ~PendingQueue, which this replaces + dropperClass = default("inet::ieee80211::Ieee80211LongestFlowDropper"); // on overflow, drop the tail frame of the longest per-station backlog + displayStringTextFormat = default("{numStations} stations\ncontains %p pk (%l) dropped %d"); + scheduler.fairnessEnabled = default(this.fairnessEnabled); + submodules: + // Generic dynamic classifier: routes by receiver MAC (Ieee80211ReceiverAddressClassifier) + // and creates a PerStationAirtimeQueue (queue -> gate) for each new station as branch[k], + // wiring it into the scheduler. + classifier: DynamicClassifier { + classifierClass = default("inet::ieee80211::Ieee80211ReceiverAddressClassifier"); + moduleType = default("inet.linklayer.ieee80211.mac.queue.PerStationAirtimeQueue"); + submoduleName = default("branch"); + aggregatorSubmoduleName = default("scheduler"); + @display("p=100,150"); + } + // Grown on demand, one sub-queue and airtime gate per receiver. The branch is created + // with its final name and index, so a station can also be configured individually, as + // in branch[2].weight = 2. + branch[0]: PerStationAirtimeQueue { + quantum = default(parent.quantum); + weight = default(parent.weight); + fairnessEnabled = default(parent.fairnessEnabled); + subqueueTypename = default(parent.subqueueTypename); + @display("p=400,150,column,80"); + } + scheduler: AirtimeFairnessScheduler { + @display("p=700,150"); + } + connections allowunconnected: + in --> { @display("m=w"); } --> classifier.in; + scheduler.out --> { @display("m=e"); } --> out; +} diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.cc b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.cc new file mode 100644 index 00000000000..03eed1754a6 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.cc @@ -0,0 +1,192 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include "inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.h" + +#include "inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.h" + +namespace inet { +namespace ieee80211 { + +Define_Module(AirtimeFairnessScheduler); + +void AirtimeFairnessScheduler::initialize(int stage) +{ + PacketSchedulerBase::initialize(stage); + if (stage == INITSTAGE_LOCAL) { + fairnessEnabled = par("fairnessEnabled"); + resolveGates(); + // The per-station branches are wired up as the stations appear (see + // ~DynamicClassifier), so watch for an input gate of ours being connected. Only + // connections made after this point are reported; the ones coming from the NED + // topology are already in inputGates. + subscribe(POST_MODEL_CHANGE, this); + WATCH(cursor); + } +} + +void AirtimeFairnessScheduler::receiveSignal(cComponent *source, simsignal_t signalID, cObject *object, cObject *details) +{ + if (signalID == POST_MODEL_CHANGE) { + // A connection ending at one of our input gates was just created; the notification + // is fired on the module owning the path end gate, that is, on us. + if (auto notification = dynamic_cast(object)) { + cGate *inputGate = notification->pathEndGate; + if (inputGate->getOwnerModule() == this && !strcmp(inputGate->getBaseName(), "in")) + addInput(inputGate); + } + } +} + +void AirtimeFairnessScheduler::resolveGates() +{ + // Resolve the AirtimeFairnessGate feeding each input gate (matched pair). The gate is at + // the far end of the connection path (branch[i].gate.out --> ... --> in[i]), which is not + // the same as the previous gate: the branch is a compound module. + gates.resize(inputGates.size()); + for (size_t i = 0; i < inputGates.size(); i++) + gates[i] = check_and_cast(inputGates[i]->getPathStartGate()->getOwnerModule()); +} + +void AirtimeFairnessScheduler::addInput(cGate *inputGate) +{ + Enter_Method("addInput"); + inputGates.push_back(inputGate); + queueing::PassivePacketSourceRef provider; + provider.reference(inputGate, false); + providers.push_back(provider); + queueing::ActivePacketSourceRef producer; + producer.reference(inputGate, false); + producers.push_back(producer); + gates.push_back(check_and_cast(inputGate->getPathStartGate()->getOwnerModule())); + checkPacketOperationSupport(inputGate); + // The downstream is deliberately not notified here: the branch is still empty (and its + // modules are not initialized yet, so it must not be looked into), and the frame whose + // arrival created the station will notify through the branch a moment later anyway. +} + +int AirtimeFairnessScheduler::schedulePacket() +{ + int n = (int)inputGates.size(); + if (n == 0) + throw cRuntimeError("The scheduler has no input gates"); + // Deficit round robin: starting at the cursor, find the next backlogged station whose + // gate is still eligible (open), topping up ineligible-but-backlogged stations by one + // quantum as we pass them. Guaranteed to terminate because quantum * weight > 0 and at + // least one station is backlogged whenever pullPacket is called. + int emptyStreak = 0; + while (true) { + int index = cursor % n; + auto gate = getGate(index); + if (gate->isBacklogged()) { + emptyStreak = 0; + if (!fairnessEnabled || gate->getDeficit() >= SIMTIME_ZERO) { + // Eligible -> serve this station. Airtime-fair keeps the cursor on it so it + // drains its whole airtime quantum (many small frames) before yielding; the + // async airtime charge lands before the next pull and eventually closes its + // gate, at which point it is topped up and rotated. Frame-fair rotates now. + // + // Pinning the cursor is only safe while the station is actually being charged + // for what it sends. Serving it again without its deficit having moved means + // the previous frame was never billed -- group-addressed frames are not + // reported by ~Dcf/~Hcf, and neither is a frame that is dequeued but never + // transmitted -- and holding the cursor there would starve every other + // station forever. Rotate away instead, so an unbilled station gets frame + // fairness rather than the whole medium. + bool charged = index != lastServedIndex || gate->getDeficit() < lastServedDeficit; + cursor = fairnessEnabled && charged ? index : (index + 1) % n; + lastServedIndex = index; + lastServedDeficit = gate->getDeficit(); + return index; + } + else { + // Backlogged but out of airtime credit -> grant one quantum and move on. + gate->addQuantum(); + cursor = (index + 1) % n; + } + } + else { + cursor = (index + 1) % n; + if (++emptyStreak >= n) + throw cRuntimeError("No backlogged input gate available to schedule a packet"); + } + } +} + +bool AirtimeFairnessScheduler::canPullSomePacket(const cGate *gate) const +{ + // A packet is pullable whenever any station is backlogged, even if all gates are + // momentarily closed (negative deficit): the scheduler can always top a station up. + for (int i = 0; i < (int)inputGates.size(); i++) + if (getGate(i)->isBacklogged()) + return true; + return false; +} + +Packet *AirtimeFairnessScheduler::canPullPacket(const cGate *gate) const +{ + for (int i = 0; i < (int)inputGates.size(); i++) { + auto g = getGate(i); + if (g->isBacklogged()) + return g->peekPacket(); + } + return nullptr; +} + +int AirtimeFairnessScheduler::getNumPackets() const +{ + int size = 0; + for (auto gate : gates) + size += gate->getNumPackets(); + return size; +} + +b AirtimeFairnessScheduler::getTotalLength() const +{ + b totalLength(0); + for (auto gate : gates) + totalLength += gate->getTotalLength(); + return totalLength; +} + +Packet *AirtimeFairnessScheduler::getPacket(int index) const +{ + int origIndex = index; + for (auto gate : gates) { + int numPackets = gate->getNumPackets(); + if (index < numPackets) + return gate->getPacket(index); + else + index -= numPackets; + } + throw cRuntimeError("Index %i out of range", origIndex); +} + +void AirtimeFairnessScheduler::removePacket(Packet *packet) +{ + Enter_Method("removePacket"); + for (auto gate : gates) { + int numPackets = gate->getNumPackets(); + for (int j = 0; j < numPackets; j++) { + if (gate->getPacket(j) == packet) { + gate->removePacket(packet); + return; + } + } + } + throw cRuntimeError("Cannot find packet"); +} + +void AirtimeFairnessScheduler::removeAllPackets() +{ + Enter_Method("removeAllPackets"); + for (auto gate : gates) + gate->removeAllPackets(); +} + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.h b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.h new file mode 100644 index 00000000000..b19bd5ec092 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.h @@ -0,0 +1,110 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_AIRTIMEFAIRNESSSCHEDULER_H +#define __INET_AIRTIMEFAIRNESSSCHEDULER_H + +#include + +#include "inet/queueing/base/PacketSchedulerBase.h" +#include "inet/queueing/contract/IPacketCollection.h" + +namespace inet { +namespace ieee80211 { + +class AirtimeFairnessGate; + +/** + * The scheduling half of the ~AirtimeFairnessQueue compound queue; the counterpart of + * ~AirtimeFairnessGate. It runs a deficit round robin (DRR) over the per-station gates, + * as in Linux `mac80211` airtime fairness: it visits the gates in round-robin order and + * serves the first backlogged station that is still eligible (its gate is open, i.e. its + * airtime deficit is non-negative); an ineligible-but-backlogged station is granted one + * `quantum * weight` of airtime credit (see AirtimeFairnessGate::addQuantum()) and the + * cursor moves on. Over time every backlogged station gets an equal share of on-air time. + * + * The airtime deficit itself lives in the gates, charged a-posteriori from the actual + * on-air duration of each transmitted frame; this scheduler only owns the rotation and + * the top-up trigger, so the two form a matched pair. Because a closed gate hides its + * backlog from the generic pull interface, the scheduler consults the gates through their + * typed interface (isBacklogged()/getDeficit()/peekPacket()) rather than the plain + * provider references, and reports a pullable packet whenever *any* station is backlogged + * (even if every gate is momentarily closed) since it can always top a station up. + * + * Stations come and go, so the input gates are not all known at network setup time: the + * ~DynamicClassifier of the enclosing queue wires up a new branch as each receiver first + * appears. The scheduler picks such an input up by listening for the model change + * notification of its own input gate being connected, rather than by being called into. + * + * Like the other schedulers used inside a compound queue, it also implements + * ~IPacketCollection, aggregating the per-station backlog so the enclosing + * ~CompoundPacketQueueBase can report queue length and enforce the shared capacity. + * + * With `fairnessEnabled = false` the gates stay open and this degrades to a plain + * per-station round robin (the frame-fair anomaly baseline). + * + * See the corresponding NED file for more details. + * + * @see AirtimeFairnessQueue, AirtimeFairnessGate + */ +class INET_API AirtimeFairnessScheduler : public queueing::PacketSchedulerBase, public virtual queueing::IPacketCollection, public cListener +{ + protected: + // parameters + bool fairnessEnabled = true; // when false, degrades to a plain per-station round robin + + // state + int cursor = 0; // round-robin position: the input gate index considered first + int lastServedIndex = -1; // input gate index served by the previous schedulePacket() call + simtime_t lastServedDeficit = SIMTIME_ZERO; // deficit of that station when it was served + std::vector gates; // the gate feeding each input gate, parallel to inputGates + + protected: + virtual void initialize(int stage) override; + virtual int schedulePacket() override; + virtual void resolveGates(); + + virtual AirtimeFairnessGate *getGate(int index) const { return gates[index]; } + + /** + * Takes an input gate connected at runtime (a per-station branch created on demand by + * ~DynamicClassifier) into the rotation: appends the provider/producer references and + * the matched gate. + */ + virtual void addInput(cGate *inputGate); + + public: + /** + * Notices this scheduler's own input gates being connected at runtime; see addInput(). + */ + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *object, cObject *details) override; + + /** @name Pull interface */ + //@{ + virtual bool canPullSomePacket(const cGate *gate) const override; + virtual Packet *canPullPacket(const cGate *gate) const override; + //@} + + /** @name IPacketCollection: aggregate per-station backlog for the compound queue */ + //@{ + virtual int getMaxNumPackets() const override { return -1; } + virtual int getNumPackets() const override; + + virtual b getMaxTotalLength() const override { return b(-1); } + virtual b getTotalLength() const override; + + virtual bool isEmpty() const override { return getNumPackets() == 0; } + virtual Packet *getPacket(int index) const override; + virtual void removePacket(Packet *packet) override; + virtual void removeAllPackets() override; + //@} +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.ned b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.ned new file mode 100644 index 00000000000..29289758c47 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessScheduler.ned @@ -0,0 +1,37 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.linklayer.ieee80211.mac.queue; + +import inet.queueing.base.PacketSchedulerBase; +import inet.queueing.contract.IPacketScheduler; + +// +// The scheduling half of the ~AirtimeFairnessQueue compound queue; the counterpart of +// ~AirtimeFairnessGate. It runs a deficit round robin over the per-station gates, as in +// Linux `mac80211` airtime fairness: it serves the first backlogged station whose gate is +// still open (airtime deficit non-negative), and grants an ineligible-but-backlogged +// station one `quantum * weight` of airtime credit before moving the cursor on. Over time +// every backlogged station gets an equal share of on-air time. +// +// The airtime deficit lives in the gates (charged a-posteriori from actual on-air time); +// this scheduler owns only the rotation and the top-up trigger, so the two form a matched +// pair and the scheduler must be wired to ~AirtimeFairnessGate inputs. Those inputs are +// wired up as the stations appear, so the scheduler also takes an input gate of its own +// being connected at runtime as a new station joining the rotation. +// +// With `fairnessEnabled = false` the gates stay open and this degrades to a plain +// per-station round robin (the frame-fair anomaly baseline). +// +// @see ~AirtimeFairnessQueue, ~AirtimeFairnessGate, ~DynamicClassifier +// +simple AirtimeFairnessScheduler extends PacketSchedulerBase like IPacketScheduler +{ + parameters: + bool fairnessEnabled = default(true); // when false, degrades to a plain per-station round robin + @class(AirtimeFairnessScheduler); +} diff --git a/src/inet/linklayer/ieee80211/mac/queue/Ieee80211LongestFlowDropper.cc b/src/inet/linklayer/ieee80211/mac/queue/Ieee80211LongestFlowDropper.cc new file mode 100644 index 00000000000..fa0b49c22b1 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/Ieee80211LongestFlowDropper.cc @@ -0,0 +1,50 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include + +#include "inet/linklayer/common/MacAddress.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/function/PacketDropperFunction.h" + +namespace inet { +namespace ieee80211 { + +/** + * The overflow policy for the ~AirtimeFairnessQueue compound queue. When the shared + * capacity is exceeded it drops from the station (receiver) with the most queued frames: + * the tail frame of the longest per-station backlog. This caps a slow station whose + * slowly-draining backlog would otherwise fill the shared capacity and lock the other + * stations out (the FQ-CoDel drop-from-longest rule), rather than dropping the just-arrived + * frame -- which is what lets every station keep getting served under overload. + */ +static Packet *selectPacketFromLongestFlow(queueing::IPacketCollection *collection) +{ + int numPackets = collection->getNumPackets(); + std::map counts; + std::map lastPacket; // tail frame of each receiver, in collection order + for (int i = 0; i < numPackets; i++) { + auto packet = collection->getPacket(i); + MacAddress receiver = packet->peekAtFront()->getReceiverAddress(); + counts[receiver]++; + lastPacket[receiver] = packet; + } + MacAddress longest; + int most = 0; + for (auto& element : counts) + if (element.second > most) { + most = element.second; + longest = element.first; + } + return most > 0 ? lastPacket[longest] : nullptr; +} + +Register_Packet_Dropper_Function(Ieee80211LongestFlowDropper, selectPacketFromLongestFlow); + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/queue/Ieee80211ReceiverAddressClassifier.cc b/src/inet/linklayer/ieee80211/mac/queue/Ieee80211ReceiverAddressClassifier.cc new file mode 100644 index 00000000000..a320d9481d9 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/Ieee80211ReceiverAddressClassifier.cc @@ -0,0 +1,44 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include + +#include "inet/linklayer/common/MacAddress.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/queueing/contract/IPacketClassifierFunction.h" + +namespace inet { +namespace ieee80211 { + +/** + * A packet classifier function that classifies IEEE 802.11 frames by receiver (destination + * station) MAC address, assigning a dense class index in first-seen order. Used with + * ~DynamicClassifier to give one per-receiver branch (sub-queue + airtime gate) in + * ~AirtimeFairnessQueue. + */ +class INET_API Ieee80211ReceiverAddressClassifier : public cObject, public virtual queueing::IPacketClassifierFunction +{ + protected: + mutable std::map addressToIndex; // receiver MAC -> dense class index, first-seen order + + public: + virtual int classifyPacket(Packet *packet) const override + { + MacAddress address = packet->peekAtFront()->getReceiverAddress(); + auto it = addressToIndex.find(address); + if (it != addressToIndex.end()) + return it->second; + int index = (int)addressToIndex.size(); + addressToIndex[address] = index; + return index; + } +}; + +Register_Class(Ieee80211ReceiverAddressClassifier); + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/queue/PerStationAirtimeQueue.ned b/src/inet/linklayer/ieee80211/mac/queue/PerStationAirtimeQueue.ned new file mode 100644 index 00000000000..a1b5f5c9007 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/queue/PerStationAirtimeQueue.ned @@ -0,0 +1,49 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.linklayer.ieee80211.mac.queue; + +import inet.common.Module; +import inet.queueing.contract.IPacketQueue; + +// +// One per-station branch of the ~AirtimeFairnessQueue: a FIFO sub-queue followed by an +// ~AirtimeFairnessGate. ~AirtimeFairnessQueue's ~DynamicClassifier instantiates one of these +// per receiver on demand, as `branch[k]`, and the ~AirtimeFairnessScheduler finds the gate of +// each branch at the far end of the connection into its own input gate. The +// `quantum`/`weight`/`fairnessEnabled`/`subqueueTypename` parameters default to those of the +// enclosing ~AirtimeFairnessQueue, and may also be set per station, as in `branch[2].weight`. +// +module PerStationAirtimeQueue extends Module like IPacketQueue +{ + parameters: + // the queue icon the other ~IPacketQueue modules carry (inherited from + // ~PacketQueueBase, which this one cannot extend, being a compound module) + @display("i=block/queue"); + // the state of the station, which was visible on the sub-queue and the gate directly + // while the branch was flattened into the queue: the airtime deficit held by the + // gate, and the frames waiting in the sub-queue; the leading dot makes the submodule + // references relative (a bare `gate.deficit` would be looked up as an absolute path) + displayStringTextFormat = default("deficit {.gate.deficit}\ncontains {.queue.numPackets} pk ({.queue.totalLength})"); + double quantum @unit(s) = default(1500us); + double weight = default(1); + bool fairnessEnabled = default(true); + string subqueueTypename = default("inet.queueing.queue.PacketQueue"); + gate.quantum = default(this.quantum); + gate.weight = default(this.weight); + gate.fairnessEnabled = default(this.fairnessEnabled); + gates: + input in @labels(push); + output out @labels(pull); + submodules: + queue: like IPacketQueue; + gate: AirtimeFairnessGate; + connections: + in --> queue.in; + queue.out --> gate.in; + gate.out --> out; +} diff --git a/src/inet/queueing/base/PacketClassifierBase.cc b/src/inet/queueing/base/PacketClassifierBase.cc index c221362e2c5..39a64b584f8 100644 --- a/src/inet/queueing/base/PacketClassifierBase.cc +++ b/src/inet/queueing/base/PacketClassifierBase.cc @@ -60,11 +60,21 @@ int PacketClassifierBase::callClassifyPacket(Packet *packet) const { // KLUDGE int index = const_cast(this)->classifyPacket(packet); - if (index < 0 || static_cast(index) >= outputGates.size()) + if (index < -1 || index >= (int)outputGates.size()) throw cRuntimeError("Packet is classified to invalid output gate: %d", index); return index; } +int PacketClassifierBase::createGateForPacket(Packet *packet) +{ + throw cRuntimeError("Packet cannot be classified to any output gate"); +} + +bool PacketClassifierBase::canCreateGateForPacket(Packet *packet) const +{ + return false; +} + void PacketClassifierBase::checkPacketStreaming(Packet *packet) { if (inProgressStreamId != -1 && (packet == nullptr || packet->getTreeId() != inProgressStreamId)) @@ -76,6 +86,8 @@ void PacketClassifierBase::startPacketStreaming(Packet *packet) EV_INFO << "Classifying packet" << EV_FIELD(packet) << EV_ENDL; inProgressStreamId = packet->getTreeId(); inProgressGateIndex = callClassifyPacket(packet); + if (inProgressGateIndex == -1) + inProgressGateIndex = createGateForPacket(packet); } void PacketClassifierBase::endPacketStreaming(Packet *packet) @@ -97,6 +109,8 @@ bool PacketClassifierBase::canPushSomePacket(const cGate *gate) const bool PacketClassifierBase::canPushPacket(Packet *packet, const cGate *gate) const { int index = callClassifyPacket(packet); + if (index == -1) + return canCreateGateForPacket(packet); return consumers[index].canPushPacket(packet); } @@ -107,6 +121,8 @@ void PacketClassifierBase::pushPacket(Packet *packet, const cGate *gate) checkPacketStreaming(nullptr); EV_INFO << "Classifying packet" << EV_FIELD(packet) << EV_ENDL; int index = callClassifyPacket(packet); + if (index == -1) + index = createGateForPacket(packet); handlePacketProcessed(packet); emit(packetPushedSignal, packet); pushOrSendPacket(packet, outputGates[index], consumers[index]); @@ -219,6 +235,8 @@ void PacketClassifierBase::handleCanPullPacketChanged(const cGate *gate) auto packet = provider.canPullPacket(); if (packet != nullptr) { int index = callClassifyPacket(packet); + if (index == -1) + return; // the packet routes to no existing gate, so there is no collector to notify auto collector = collectors[index]; if (collector != nullptr) collector.handleCanPullPacketChanged(); diff --git a/src/inet/queueing/base/PacketClassifierBase.h b/src/inet/queueing/base/PacketClassifierBase.h index 7556ec6b89e..1732771f38a 100644 --- a/src/inet/queueing/base/PacketClassifierBase.h +++ b/src/inet/queueing/base/PacketClassifierBase.h @@ -43,9 +43,33 @@ class INET_API PacketClassifierBase : public PacketProcessorBase, public Transpa virtual void mapRegistrationForwardingGates(cGate *gate, std::function f) override; virtual size_t getOutputGateIndex(size_t i) const { return reverseOrder ? outputGates.size() - i - 1 : i; } + + /** + * Returns the index of the output gate the packet is classified to, or -1 + * if no existing output gate suits the packet. Classification is a query + * and must be free of side effects: the capacity checks (canPushPacket(), + * canPullPacket()) classify the same packet as its eventual delivery, and + * the pull path classifies it more than once. + */ virtual int classifyPacket(Packet *packet) = 0; virtual int callClassifyPacket(Packet *packet) const; + /** + * Called when a packet being pushed is classified to no existing output + * gate. This is where side effects of taking such a packet belong: a + * classifier that extends itself on demand creates the new output gate + * here and returns its index. Called from packet delivery only, never + * from a query. The default refuses the packet with an error. + */ + virtual int createGateForPacket(Packet *packet); + + /** + * Returns true if createGateForPacket() would provide an output gate for + * the packet: the query pair of createGateForPacket(), consulted by + * canPushPacket() when classifyPacket() finds no gate. + */ + virtual bool canCreateGateForPacket(Packet *packet) const; + virtual bool isStreamingPacket() const { return inProgressStreamId != -1; } virtual void startPacketStreaming(Packet *packet); virtual void endPacketStreaming(Packet *packet); diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 43d6fe71c02..644f0416edf 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -21,44 +21,105 @@ void DynamicClassifier::initialize(int stage) if (stage == INITSTAGE_LOCAL) { submoduleName = par("submoduleName"); moduleType = cModuleType::get(par("moduleType")); + aggregatorSubmoduleName = par("aggregatorSubmoduleName"); if (!getParentModule()->hasSubmoduleVector(submoduleName)) - throw cRuntimeError("The submodule vector '%s' missing from %s", submoduleName, getParentModule()->getFullPath().c_str()); - } + throw cRuntimeError("The submodule vector '%s' is missing from %s", submoduleName, getParentModule()->getFullPath().c_str()); + if (getParentModule()->getSubmodule(aggregatorSubmoduleName) == nullptr) + throw cRuntimeError("The aggregator submodule '%s' is missing from %s", aggregatorSubmoduleName, getParentModule()->getFullPath().c_str()); + } +} + +int DynamicClassifier::getClassIndex(Packet *packet) const +{ + // The class of the packet, taken as the classifier function returns it, and not mapped + // through getOutputGateIndex(): that mapping depends on the number of output gates, which + // grows with each branch, so the same class would end up under a different key over time, + // and get a second branch. + return packetClassifierFunction->classifyPacket(packet); } int DynamicClassifier::classifyPacket(Packet *packet) { - int index = PacketClassifier::classifyPacket(packet); - auto it = classIndexToGateItMap.find(index); - if (it == classIndexToGateItMap.end()) { - auto parentModule = getParentModule(); - int submoduleIndex = gateSize("out"); - int origVectorSize = parentModule->getSubmoduleVectorSize(submoduleName); - parentModule->setSubmoduleVectorSize(submoduleName, std::max(origVectorSize, submoduleIndex + 1)); - auto module = moduleType->create(submoduleName, parentModule, submoduleIndex); - auto moduleInputGate = module->gate("in"); - auto moduleOutputGate = module->gate("out"); - auto multiplexer = parentModule->getSubmodule("multiplexer"); - multiplexer->setGateSize("in", multiplexer->gateSize("in") + 1); - auto multiplexerInputGate = multiplexer->gate("in", multiplexer->gateSize("in") - 1); - setGateSize("out", submoduleIndex + 1); - auto classifierOutputGate = gate("out", gateSize("out") - 1); - classifierOutputGate->connectTo(moduleInputGate); - outputGates.push_back(classifierOutputGate); - PassivePacketSinkRef consumer; - consumer.reference(classifierOutputGate, false); - consumers.push_back(consumer); - moduleOutputGate->connectTo(multiplexerInputGate); - module->finalizeParameters(); - module->buildInside(); + // a class seen for the first time has no gate yet; its branch is created by + // createGateForPacket(), which the base class calls on packet delivery only + auto it = classIndexToGateItMap.find(getClassIndex(packet)); + return it != classIndexToGateItMap.end() ? it->second : -1; +} + +int DynamicClassifier::createGateForPacket(Packet *packet) +{ + int branchIndex = createBranch(); + classIndexToGateItMap[getClassIndex(packet)] = branchIndex; + return branchIndex; +} + +bool DynamicClassifier::canCreateGateForPacket(Packet *packet) const +{ + // a branch can be created for every class, so every packet gets an output gate + return true; +} + +bool DynamicClassifier::canPushSomePacket(const cGate *gate) const +{ + // Not the inherited "one of the existing branches can take a packet": a packet of a class + // that has not been seen yet is taken by the branch created for it, and there may always be + // such a class, the range of the classifier function not being known here. Without this, a + // classifier that has no branch yet answers that it cannot accept anything, and an active + // source in front of it stops before the first branch is ever created. Whether a particular + // packet can be pushed is answered by the inherited canPushPacket(), through + // canCreateGateForPacket() above. + return true; +} + +int DynamicClassifier::createBranch() +{ + cModule *parent = getParentModule(); + int index = gateSize("out"); + // grow this classifier's output gate vector + setGateSize("out", index + 1); + cGate *classifierOutputGate = gate("out", index); + // build the branch and collect the modules whose initialization is deferred until the + // whole chain (including the aggregator connection) is wired + std::vector modulesToInitialize; + cGate *branchOutputGate = createModuleBranch(index, classifierOutputGate, modulesToInitialize); + // Wire the branch output into the aggregator's next input gate. An aggregator that has + // to take notice of a runtime-added input (a pull scheduler, for example) learns about + // it from the model change notification of this very connection, so nothing here needs + // to know what kind of aggregator it is. + cModule *aggregator = parent->getSubmodule(aggregatorSubmoduleName); + aggregator->setGateSize("in", aggregator->gateSize("in") + 1); + cGate *aggregatorInputGate = aggregator->gate("in", aggregator->gateSize("in") - 1); + branchOutputGate->connectTo(aggregatorInputGate); + // the sink references resolve the far end of the path eagerly, so they can only be taken + // now that the whole branch, up to and including the aggregator, is connected + outputGates.push_back(classifierOutputGate); + PassivePacketSinkRef consumer; + consumer.reference(classifierOutputGate, false); + consumers.push_back(consumer); + ActivePacketSinkRef collector; + collector.reference(classifierOutputGate, false); + collectors.push_back(collector); + for (auto module : modulesToInitialize) module->callInitialize(); - classIndexToGateItMap[index] = submoduleIndex; - return submoduleIndex; - } - else - return it->second; + return index; +} + +cGate *DynamicClassifier::createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize) +{ + cModule *parent = getParentModule(); + // the vector is only ever extended: it may have been declared larger in NED, and shrinking + // one that still holds submodules is an error + parent->setSubmoduleVectorSize(submoduleName, std::max(parent->getSubmoduleVectorSize(submoduleName), index + 1)); + // the branch is created with its final name and index, so that its parameters (from the + // enclosing NED declaration and from the ini file), its display string and its result + // recording are all resolved for the module path it keeps + cModule *module = moduleType->create(submoduleName, parent, index); + classifierOutputGate->connectTo(module->gate("in")); + module->finalizeParameters(); + module->buildInside(); + modulesToInitialize.push_back(module); + return module->gate("out"); } } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index e1907fe1e55..66dc3f59998 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -8,6 +8,8 @@ #ifndef __INET_DYNAMICCLASSIFIER_H #define __INET_DYNAMICCLASSIFIER_H +#include + #include "inet/queueing/classifier/PacketClassifier.h" namespace inet { @@ -15,20 +17,42 @@ namespace queueing { using namespace inet::queueing; +/** + * A packet classifier that creates the branch for each traffic class on demand, the first + * time a packet of that class is seen. Each branch is one element of a submodule vector + * (submoduleName) of a configurable type (moduleType), wired between this classifier's output + * and a downstream aggregator submodule (aggregatorSubmoduleName). + * + * The aggregator may be either a push ~PacketMultiplexer (the traditional use) or a pull + * scheduler. An aggregator that needs to take notice of an input appearing at runtime picks + * it up from the POST_MODEL_CHANGE notification of the connection itself (see + * cPostPathCreateNotification), so no extra contract is needed between the two. This lets the + * same classifier build both push demux/remux chains and pull per-class queue/scheduler + * structures. + */ class INET_API DynamicClassifier : public PacketClassifier { protected: - const char *submoduleName = nullptr; - cModuleType *moduleType = nullptr; + const char *submoduleName = nullptr; // submodule vector that holds the branches + cModuleType *moduleType = nullptr; // type of the per-class branch module (may be a compound) + const char *aggregatorSubmoduleName = nullptr; // downstream aggregator submodule (multiplexer or scheduler) std::map classIndexToGateItMap; protected: virtual void initialize(int stage) override; + virtual int getClassIndex(Packet *packet) const; virtual int classifyPacket(Packet *packet) override; + virtual int createGateForPacket(Packet *packet) override; + virtual bool canCreateGateForPacket(Packet *packet) const override; + + virtual int createBranch(); + virtual cGate *createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); + + public: + virtual bool canPushSomePacket(const cGate *gate) const override; }; } // namespace queueing } // namespace inet #endif - diff --git a/src/inet/queueing/classifier/DynamicClassifier.ned b/src/inet/queueing/classifier/DynamicClassifier.ned index 9a679e097ce..7cbf88dd536 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.ned +++ b/src/inet/queueing/classifier/DynamicClassifier.ned @@ -7,10 +7,26 @@ package inet.queueing.classifier; +// +// A packet classifier that creates the branch for each traffic class on demand. Each branch is +// one element of the `submoduleName` submodule vector, of type `moduleType` (which may be a +// compound module), wired between this classifier's output and a downstream aggregator +// submodule (`aggregatorSubmoduleName`, a push multiplexer by default). The aggregator may also +// be a pull scheduler; one that has to take notice of an input appearing at runtime learns +// about it from the model change notification of the connection being made, so no extra +// contract is needed between the two. +// +// The submodule vector must be declared in the enclosing compound module, where it may be +// empty; the classifier extends it as branches are created. A branch is created with its final +// name and index, so parameter assignments (both from the enclosing NED declaration and from +// the ini file), display string configuration and result recording all address it as +// `[k]`. +// simple DynamicClassifier extends PacketClassifier { parameters: - string submoduleName; - string moduleType; + string moduleType; // NED type of the per-class branch module (may be a compound) + string submoduleName; // the submodule vector that holds the branches + string aggregatorSubmoduleName = default("multiplexer"); // downstream aggregator submodule to wire branches into @class(DynamicClassifier); } diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.cc b/src/inet/queueing/queue/CompoundPacketQueueBase.cc index d52d3842a69..99fdf212d19 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.cc +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.cc @@ -61,7 +61,14 @@ void CompoundPacketQueueBase::pushPacket(Packet *packet, const cGate *gate) while (isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; - removePacket(packet); + // Remove the victim directly from the underlying collection instead of via + // removePacket(): removePacket() also emits packetRemoved, which would count the + // drop as both a removal and a drop, subtracting it twice from the queue-length + // statistic. The victim is still owned by the submodule it was queued in, so take + // it before dropPacket() deletes it. + collection->removePacket(packet); + if (packet->getOwner() != this) + take(packet); dropPacket(packet, QUEUE_OVERFLOW); } } diff --git a/tests/module/AirtimeFairnessQueue_1.test b/tests/module/AirtimeFairnessQueue_1.test new file mode 100644 index 00000000000..75a33d15359 --- /dev/null +++ b/tests/module/AirtimeFairnessQueue_1.test @@ -0,0 +1,97 @@ +%description: +Tests that AirtimeFairnessQueue shares the access point's downlink airtime fairly +among stations instead of sharing frames fairly. + +An access point saturates three stations at the same 54 Mbps PHY rate; sta[0] +receives long (1400 B) frames while sta[1]/sta[2] receive short (200 B) ones, so +one frame to sta[0] occupies several times the airtime of a frame to the others. +A frame-fair round robin (fairnessEnabled = false) serves roughly equal frames +per station, which gives sta[0] about 7x the bytes of a short-frame station +(measured byte ratio 0.14). With airtime fairness, and a quantum fine enough for +the deficit to bind on every frame, the short-frame stations recover to well +over 0.25x of sta[0]'s bytes (measured 0.37), while every station keeps being +served. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.inet.StandardHost; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +network AirtimeFairnessTest +{ + parameters: + int numStations = default(3); + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + server: StandardHost; + accessPoint: AccessPoint; + sta[numStations]: WirelessHost; + connections: + accessPoint.ethg++ <--> Eth100M <--> server.ethg++; +} + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +network = AirtimeFairnessTest +ned-path = .;../../../../src +sim-time-limit = 2.5s +warmup-period = 1s +cmdenv-express-mode = true +**.vector-recording = false + +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +*.*.ipv4.arp.typename = "GlobalArp" + +# equally good links, all stations ~8-9 m from the AP +*.sta[*].mobility.typename = "StationaryMobility" +**.mobility.initFromDisplayString = false +*.sta[*].mobility.initialX = 9m +*.sta[*].mobility.initialY = 8m + parentIndex() * 2m +*.sta[*].mobility.initialZ = 0m +*.accessPoint.mobility.initialX = 17m +*.accessPoint.mobility.initialY = 12m +*.accessPoint.mobility.initialZ = 0m + +# 802.11g, one shared rate for every frame +*.sta[*].wlan[*].opMode = "g(erp)" +*.accessPoint.wlan[*].opMode = "g(erp)" +*.sta[*].wlan[*].bitrate = 54Mbps +*.accessPoint.wlan[*].bitrate = 54Mbps +*.sta[*].wlan[*].radio.transmitter.power = 100mW +*.accessPoint.wlan[*].radio.transmitter.power = 100mW + +# saturating downlink: long frames to sta[0], short ones to sta[1]/sta[2] +*.server.numApps = 3 +*.server.app[*].typename = "UdpBasicApp" +*.server.app[*].destAddresses = "sta[" + string(index) + "]" +*.server.app[*].destPort = 5000 + index +*.server.app[*].messageLength = index == 0 ? 1400B : 200B +*.server.app[*].sendInterval = index == 0 ? 0.3ms : 0.1ms +*.sta[*].numApps = 1 +*.sta[*].app[0].typename = "UdpSink" +*.sta[*].app[0].localPort = 5000 + parentIndex() + +# unrelated to this test: UtilizationFilter asserts in debug builds when an Ethernet +# transmission is in progress at the moment the warmup period expires +**.eth[*].**.statistic-recording = false + +# the airtime-fair transmit queue under test; the fine-grained quantum makes the +# airtime deficit bind on every frame +*.accessPoint.wlan[*].mac.dcf.channelAccess.pendingQueue.typename = "AirtimeFairnessQueue" +*.accessPoint.wlan[*].mac.dcf.channelAccess.pendingQueue.packetCapacity = 50 +*.accessPoint.wlan[*].mac.dcf.channelAccess.pendingQueue.quantum = 100us +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: gawk '/sta\[[0-9]+\]\.app\[0\] packetReceived:sum\(packetBytes\)/ { match($2, /sta\[([0-9]+)\]/, m); b[m[1]] = $4 } END { printf "allServed=%s\n", (b[0]>0 && b[1]>0 && b[2]>0) ? "YES" : "NO"; printf "airtimeShared=%s\n", (b[1] > b[0]*0.25 && b[2] > b[0]*0.25) ? "YES" : "NO"; printf "values sta0=%d sta1=%d sta2=%d\n", b[0], b[1], b[2] }' results/*.sca > verdict.out +%contains: verdict.out +allServed=YES +airtimeShared=YES +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: diff --git a/tests/queueing/DynamicClassifier_1.test b/tests/queueing/DynamicClassifier_1.test new file mode 100644 index 00000000000..cb0ef14bedd --- /dev/null +++ b/tests/queueing/DynamicClassifier_1.test @@ -0,0 +1,130 @@ +%description: + +In this test, packets are produced periodically by an active packet source (ActivePacketSource) +and are classified into two classes by a dynamic classifier (DynamicClassifier). The classifier +creates the branch of a class when the first packet of that class arrives, as one element of the +branch submodule vector, and wires it into the packet multiplexer that aggregates the branches. + +The producer is connected to the classifier directly, so the test also covers that a classifier +which has no branch yet accepts a packet, rather than stopping the producer before the first +branch is created. + +The branch is created with its final name and index, so the test checks that an ini file +assignment addressing a submodule of a branch (the delay of the packet delayer in it) takes +effect, and that the statistics of the branch submodules are recorded under the branch path. +Empty output vectors are turned off, so a vector appears in the result file only if data was +recorded into it. + +%file: test.ned + +import inet.queueing.classifier.DynamicClassifier; +import inet.queueing.common.BackPressureBarrier; +import inet.queueing.common.PacketDelayer; +import inet.queueing.common.PacketMultiplexer; +import inet.queueing.sink.PassivePacketSink; +import inet.queueing.source.ActivePacketSource; + +module TestBranch +{ + gates: + input in; + output out; + submodules: + first: BackPressureBarrier { + @display("p=100,100"); + } + second: PacketDelayer { + delay = default(0s); + @display("p=200,100"); + } + connections: + in --> first.in; + first.out --> second.in; + second.out --> out; +} + +module TestDemultiplexer +{ + gates: + input in; + output out; + submodules: + classifier: DynamicClassifier { + moduleType = "TestBranch"; + submoduleName = "branch"; + @display("p=100,100"); + } + branch[0]: TestBranch { // grown on demand, one branch per class + @display("p=250,100,column,80"); + } + multiplexer: PacketMultiplexer { + @display("p=400,100"); + } + connections allowunconnected: + in --> classifier.in; + multiplexer.out --> out; +} + +network TestDynamicClassifier +{ + submodules: + producer: ActivePacketSource { + @display("p=100,100"); + } + demultiplexer: TestDemultiplexer { + @display("p=200,100"); + } + consumer: PassivePacketSink { + @display("p=300,100"); + } + connections: + producer.out --> demultiplexer.in; + demultiplexer.out --> consumer.in; +} + +%file: Test.cc +#include "inet/queueing/function/PacketClassifierFunction.h" +#include "inet/common/packet/Packet.h" + +using namespace inet; + +static int testClassify(Packet *packet) +{ + return packet->getId() % 2; +} + +Register_Packet_Classifier_Function(TestClassifier, testClassify); + +%inifile: omnetpp.ini + +[General] +network = TestDynamicClassifier +sim-time-limit = 10s +cmdenv-event-banners = false +cmdenv-log-prefix = "At %ts %N: " +**.vector-record-empty = false +*.producer.packetLength = 1B +*.producer.productionInterval = 1s +*.demultiplexer.classifier.classifierClass = "TestClassifier" +*.demultiplexer.branch[*].second.delay = 2s + +%# remove formatting +%subst: /\x1B\[[0-9;]*m// +%# remove method call lines added in OMNeT++ 6.4 +%subst: /^At \S+ \S+: Method call [^\n]*\n//m +%#-------------------------------------------------------------------------------------------------------------- +%# the delay assigned to the branch submodule from the ini file must be applied +%contains-regex: stdout +At 0s producer: Producing packet, .*?producer-0.*? +At 1s producer: Producing packet, .*?producer-1.*? +At 2s consumer: Consuming packet, .*?producer-0.*? +At 3s consumer: Consuming packet, .*?producer-1.*? +%#-------------------------------------------------------------------------------------------------------------- +%# the modules that have data in an output vector, and the branch submodules among them +%postrun-command: grep "^vector " results/*.vec | cut -d ' ' -f 3 | sort -u > modules.out +%postrun-command: grep -E "\.branch\[" modules.out > branchmodules.out || true +%#-------------------------------------------------------------------------------------------------------------- +%contains: branchmodules.out +TestDynamicClassifier.demultiplexer.branch[0].first +TestDynamicClassifier.demultiplexer.branch[1].first +%#--------------------------------------------------------------------------------------------------------------