diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 43d6fe71c02..c005b7296d9 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -21,44 +21,103 @@ 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, with no side effect -- unlike classifyPacket() below, which + // creates the branch of a class that is seen for the first time. Note that the class index + // is taken as it is, 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); + int index = getClassIndex(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(); - module->callInitialize(); - classIndexToGateItMap[index] = submoduleIndex; - return submoduleIndex; - } - else + if (it != classIndexToGateItMap.end()) return it->second; + int branchIndex = createBranch(); + classIndexToGateItMap[index] = branchIndex; + return branchIndex; +} + +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 canPushPacket() below. + return true; +} + +bool DynamicClassifier::canPushPacket(Packet *packet, const cGate *gate) const +{ + // deliberately not the inherited implementation: that one classifies the packet, which + // creates the branch of a new class as a side effect of what is supposed to be a query + auto it = classIndexToGateItMap.find(getClassIndex(packet)); + return it == classIndexToGateItMap.end() || consumers[it->second].canPushPacket(packet); +} + +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(); + 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..79c6bc0d448 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,41 @@ 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 createBranch(); + virtual cGate *createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); + + public: + virtual bool canPushSomePacket(const cGate *gate) const override; + virtual bool canPushPacket(Packet *packet, 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/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 +%#--------------------------------------------------------------------------------------------------------------