ieee80211: make rate control adapt per receiver, add per-receiver configured rates - #1124
ieee80211: make rate control adapt per receiver, add per-receiver configured rates#1124adamgeorge309 wants to merge 6 commits into
Conversation
| if (it == stations.end()) { | ||
| State state; | ||
| state.address = receiverAddress; | ||
| state.mode = getInitialMode(); | ||
| state.increaseThreshold = par("increaseThreshold"); | ||
| it = stations.insert({receiverAddress, state}).first; | ||
| emitDatarateChangedSignal(state.address, state.mode); | ||
| } |
There was a problem hiding this comment.
🟡 A newly seen peer's rate immediately jumps one step above the configured starting rate
The per-peer rate state is created with an unset timer (State state; at src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc:45), so the periodic "increase" deadline is already in the past the moment the peer appears, and the very first transmission to it is sent one rate step above the configured starting rate.
Impact: The configured starting rate is effectively ignored for every peer first used after the increase interval, and an extra spurious rate-change value is recorded for that peer.
Timer initialised to zero makes increaseRateIfTimerIsExpired fire on first use
State::timer defaults to SIMTIME_ZERO (src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h:26). getRate() calls stateFor() and then increaseRateIfTimerIsExpired(state) (src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc:124-125), whose condition is simTime() - state.timer >= interval (default 50 ms). For any station whose state is created after t = 50 ms this is immediately true, so state.mode is bumped to the next faster mode and a second datarateChanged is emitted before the first frame is sent. Previously there was a single module-wide state created at t≈0, so this typically did not happen; with lazily created per-peer state it happens for essentially every peer. frameTransmitted() has the same path. Initialising state.timer = simTime() at creation preserves the intended "seeded from initialRate" semantics.
| if (it == stations.end()) { | |
| State state; | |
| state.address = receiverAddress; | |
| state.mode = getInitialMode(); | |
| state.increaseThreshold = par("increaseThreshold"); | |
| it = stations.insert({receiverAddress, state}).first; | |
| emitDatarateChangedSignal(state.address, state.mode); | |
| } | |
| if (it == stations.end()) { | |
| State state; | |
| state.address = receiverAddress; | |
| state.mode = getInitialMode(); | |
| state.increaseThreshold = par("increaseThreshold"); | |
| state.timer = simTime(); | |
| it = stations.insert({receiverAddress, state}).first; | |
| emitDatarateChangedSignal(state.address, state.mode); | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in a follow-up commit — confirmed, thanks.
Reproduced in examples/wireless/hiddennode with initialRate = 2Mbps and traffic starting at t = 1s: the per-station vector opened with two values at t = 1, 2 Mbps then 5.5 Mbps, and the first frame went out at 5.5 Mbps. With the timer seeded at creation it opens with a single 2 Mbps value.
OnoeRateControl has the same defect and is fixed in the same commit. It is milder there — computeMode() returns early while the station has no successful transmission yet — but it still divides by zero on the way in, leaving avgRetriesPerFrame at NaN.
There was a problem hiding this comment.
Fixed in 65474d9 (current head): the state is created with state.timer = simTime(), so the increase interval starts when the station is first seen rather than at t=0.
| for (cModule::SubmoduleIterator it(getSimulation()->getSystemModule()); !it.end(); ++it) { | ||
| cModule *node = *it; | ||
| if (!isNetworkNode(node)) | ||
| continue; | ||
| auto interfaceTable = resolver.findInterfaceTableOf(node); | ||
| if (interfaceTable == nullptr) | ||
| continue; | ||
| bool found = false; | ||
| for (int i = 0; i < interfaceTable->getNumInterfaces(); i++) { | ||
| if (interfaceTable->getInterface(i)->getMacAddress() == receiver) { | ||
| label = node->getFullName(); | ||
| found = true; | ||
| break; | ||
| } | ||
| } | ||
| if (found) | ||
| break; | ||
| } |
There was a problem hiding this comment.
🟡 Per-station statistics are labelled with raw MAC addresses when hosts are nested inside sub-networks
The peer name lookup only inspects the modules directly under the network (cModule::SubmoduleIterator over the system module at src/inet/linklayer/ieee80211/mac/common/StationLabelCache.cc:25), so peers placed inside any grouping module are never found and get labelled with the raw MAC address instead.
Impact: Per-station data-rate series are keyed by unreadable hardware addresses in any topology where hosts are not direct children of the network.
Non-recursive sweep vs. the existing recursive helper
INET already provides L3AddressResolver::findHostWithMacAddress() / findInterfaceWithMacAddress() (src/inet/networklayer/common/L3AddressResolver.cc:551-573), which collect network nodes recursively via doCollectNetworkNodes() (src/inet/networklayer/common/L3AddressResolver.cc:525-534) and therefore also find nodes nested inside compound sub-network modules. The new cache duplicates that logic but iterates only the immediate submodules of the system module and skips any module that is not itself a network node, so nested hosts are missed. Because the fallback result is stored in the cache, the MAC-string label is then permanent for that receiver.
| for (cModule::SubmoduleIterator it(getSimulation()->getSystemModule()); !it.end(); ++it) { | |
| cModule *node = *it; | |
| if (!isNetworkNode(node)) | |
| continue; | |
| auto interfaceTable = resolver.findInterfaceTableOf(node); | |
| if (interfaceTable == nullptr) | |
| continue; | |
| bool found = false; | |
| for (int i = 0; i < interfaceTable->getNumInterfaces(); i++) { | |
| if (interfaceTable->getInterface(i)->getMacAddress() == receiver) { | |
| label = node->getFullName(); | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (found) | |
| break; | |
| } | |
| if (cModule *node = resolver.findHostWithMacAddress(receiver)) | |
| label = node->getFullName(); |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 0248366ffe — switched to L3AddressResolver::findHostWithMacAddress(), which collects network nodes recursively.
One addition: the label is now the node's path relative to the network, not getFullName(). Once nested nodes resolve, two nodes named host in different subnetworks would share a label and their vectors would merge into one demux() series. For a node directly under the network the two strings are identical, so flat topologies keep the labels they had.
Verified with the hiddennode nodes wrapped in a plain compound module: 20-00-00-00-00-00:dataratePerStation before, subnet1.B:dataratePerStation after, and the unmodified example still records B:dataratePerStation.
There was a problem hiding this comment.
Fixed in bf8ddc1 by going one step further: StationLabelCache is removed entirely, and the label is resolved on demand with L3AddressResolver::findHostWithMacAddress() (see IRateSelection::getStationLabel()), which collects network nodes recursively, so nested hosts are found too.
05bb4f7 to
1be82e6
Compare
|
Pushed a follow-up commit for the first Devin finding. Confirmed. Per-receiver state was created with its interval timer at zero, so for a peer first seen after Reproduced in
The first frame really did go out at 5.5 Mbps before the fix, so the configured starting rate was effectively ignored for every peer.
The second finding ( |
|
Pushed a follow-up commit for the second Devin finding. Confirmed. One addition beyond the suggestion: the label is now the node's path relative to the network rather than Verified with the hiddennode nodes wrapped in a plain compound module:
The last row is the point: no existing example or showcase sees a renamed statistic. |
0248366 to
a91a713
Compare
|
Pushed bf8ddc1: dropped the StationLabelCache class. Station labels are now resolved on demand through |
getRemaining() computed now - start, which is the time already spent in the TXOP rather than the time left in it. The guard above it already handles the expired case, so the remaining time is start + limit - now. This is a correctness fix with no behavioural effect. The only caller, ~HcfFs, tests the result against zero, and reaches it only once a frame of the TXOP has already been transmitted -- so now is strictly greater than start, and elapsed and remaining are either both positive, while the TXOP is live, or both zero once it has expired. Verified by a TXOP-limit sweep whose recorded scalars are identical before and after.
bf8ddc1 to
87e64f2
Compare
~AarfRateControl and ~OnoeRateControl kept one set of adaptive state per module: a single current mode, timer, probe flag, threshold and success counter. A station transmitting to several peers therefore blended their feedback into one rate -- a retry to a distant station pushed the rate down for a nearby one, and vice versa. This is visible on an access point serving clients at different distances, which is the normal case. IRateControl::getRate() now takes the receiver address, and both algorithms key their state on it: every variable that adapts moves into a per-receiver State, while the configured thresholds and intervals stay shared. State for a station is created on first use, seeded from initialRate (or the fastest mandatory mode), and dropped when the mode set changes. The interval timer of a new station is seeded with the current time, so the periodic rate increase measures its interval from when the station was first seen -- not from t=0, which for a station first used later than one interval would put the deadline already in the past at creation. RateControlBase no longer holds a currentMode, so the watched expression that fed the display string moves to the subclasses and reports the number of stations being tracked instead.
87e64f2 to
353ab79
Compare
Naming a peer by the address a model knows it by -- to label the per-station series of a demultiplexed statistic, for example -- takes both a lookup and a naming convention, and neither belongs to the model that happens to need it first. Add L3AddressResolver::getHostNameWithMacAddress(), next to the findHostWithMacAddress() it builds on. It names the host by its path relative to the network, so that hosts of the same name in different subnetworks are distinguishable, and falls back to the MAC address string when no host owns the address.
353ab79 to
fa2bc31
Compare
The datarateChanged statistic recorded one interleaved vector per rate control module, in which the rates chosen for different peers are indistinguishable. Now that rate control adapts per receiver, the rate of an individual station is what the statistic should be able to show. datarateChanged is emitted with the receiver's station name as a named details object, and a new dataratePerStation statistic uses demux(datarateChanged) to record one data rate vector per station. The aggregate datarateChanged statistic ignores details and is unchanged. Group-addressed frames carry no per-station rate and are emitted without details, so they land only in the aggregate. This is also the observable that makes the per-receiver adaptation of the previous commit testable, so the module test for it comes with it. One source saturates a near and a far ad-hoc peer at once; the rate towards the near peer stays at 54 Mbps while the far peer's retries pull its own rate down to 24.7 Mbps. With the single set of adaptive state the two peers previously shared, both are served at a blended 29.6 Mbps instead, and the far peer receives 380 packets rather than 1350.
dataFrameBitrate fixes one rate for the whole interface, so a scenario that needs different peers reached at different rates -- the usual setup for studying rate diversity -- had to fall back on an adaptive rate control and let the channel produce the spread indirectly, which is slow to set up and hard to hold steady. ~RateSelection and ~QosRateSelection now accept dataFrameBitratePerReceiver, a map from peer interface module path to bitrate. A unicast data frame whose receiver matches an entry is sent at that rate; everything else falls back to the existing rules. The parameter is empty by default, so the capability is inert unless configured. Resolution of the paths to MAC addresses is deferred to the first transmitted data frame rather than done in initialize(): peer MAC addresses are assigned during INITSTAGE_LINK_LAYER and the order of modules within a stage is undefined, so reading them from initialize() would be a race. By the time any data frame is sent, all init stages have completed. Unresolvable paths and rates the mode set does not support fail with an error naming the offending entry.
datarateSelected reports the rate chosen for every transmitted frame, so unlike datarateChanged it also covers interface-wide fixed rates and per-receiver configured rates, not just adaptive rate control. It was emitted with the packet as details, which identifies the frame but not the peer, so the values of several stations arrived interleaved with no way to separate them. ~IRateSelection::emitDatarateSelected() now emits unicast data frames with the receiver's station label as a named details object, so a demux(datarateSelected) result filter can key a per-station series on it. The condition matches the one under which ~RateSelection applies a per-receiver configured rate, so the label always names the station whose rate is being reported. Control, management and group-addressed frames have no per-station rate and are emitted without details: they still reach the aggregate statistic. Both coordination functions emit through the shared helper rather than each formatting the signal themselves. Unlike datarateChanged, this signal fires for every transmitted frame, and resolving a station label sweeps the network's interface tables -- so the label is only computed when the signal has listeners: with result recording off, transmitting costs nothing extra.
fa2bc31 to
42d0174
Compare
Makes 802.11 rate control adapt per receiver, and adds a way to configure a fixed rate per peer.
Per-receiver rate control.
AarfRateControlandOnoeRateControlkept one set of adaptive state per module: a single current mode, timer, probe flag, threshold and success counter. A station transmitting to several peers therefore blended their feedback into one rate — a retry to a distant station pushed the rate down for a nearby one, and vice versa. This is the normal case for an access point serving clients at different distances.IRateControl::getRate()now takes the receiver address, and both algorithms key their state on it: every variable that adapts moves into a per-receiverState, while configured thresholds and intervals stay shared. State is created on first use, seeded frominitialRate(or the fastest mandatory mode), and dropped when the mode set changes.Per-station statistics.
datarateChangedis now emitted with the receiver's station label (the peer's node name, resolved and cached by the newStationLabelCache) as a named details object. The aggregatedatarateChangedstatistic ignores details and is unchanged; a newdataratePerStationstatistic usesdemux(datarateChanged)to record one vector per station.datarateSelectedis tagged the same way, which additionally covers fixed and per-receiver configured rates.Per-receiver configured rates.
dataFrameBitratefixes one rate for the whole interface.RateSelectionandQosRateSelectionnow acceptdataFrameBitratePerReceiver, a map from peer interface module path to bitrate. Resolution to MAC addresses is deferred to the first transmitted data frame, because peer MACs are assigned duringINITSTAGE_LINK_LAYERwith undefined intra-stage ordering. Empty by default, so the capability is inert unless configured.Also included: a one-line fix to
TxopProcedure::getRemaining(), which returned the elapsed time rather than the remaining time. Its only caller tests the result against zero and reaches it only after a frame of the TXOP has gone out, so this is a correctness fix with no behavioural effect. Happy to split it out if preferred.Test
Builds at every commit. Run in
examples/wireless/hiddennodewithAarfRateControl; the recorded results contain per-station vectors keyed by peer node name (B:dataratePerStation:vectoron both senders), confirming the demux path end to end.🤖 Generated with Claude Code