A driver that hands every frame it dequeues to the hardware at once holds nothing back but the airtime queue limit, so every station keeps its full limit in flight and the hardware picks the next transmitter among them. The order in which ieee80211_next_txq() serves the queues never reaches the air, and the airtime weight has no effect on the airtime a station gets. ath11k is such a driver. Add IEEE80211_HW_TX_NO_PUSHBACK for a driver to say so, and scale the per-station limits by the weight when it is set. A station with twice the default weight may keep twice the limit in flight. The scaled value is capped at aql_threshold and never drops below the configured limit. A driver without the flag is not affected. Measured on an IPQ8074 AP with ath11k, a 2x2 HE160 and a 1x1 VHT80 client both saturated downstream, BE aql_txq_limit 500/1000 us, 15 s runs, three per setting. Share of the tx airtime taken by the HE160 client in per cent, at weights 256:256, 1024:256 and 256:1024: without the flag 55.2, 54.1, 52.8 55.9, 51.9, 59.8 54.8, 56.3, 59.2 with the flag 57.8, 57.5, 55.1 73.8, 76.7, 74.4 22.6, 22.7, 22.4 Mid-run the favoured client holds 1.2 to 3.2 ms in flight and the other stays at the configured 1 ms. Ping from the AP to the HE160 client under the same load is 4.8 to 7.4 ms mean at equal weights and 18.1 to 25.1 ms at 1024:256, where it is the favoured client: the deeper queue costs it that latency. The scaled limit takes effect only where the configured one binds. At the default 5000/12000 us both clients hold several milliseconds in flight, neither queue runs empty between transmissions, and the weight does not move the split. Assisted-by: Claude:claude-opus-5 Signed-off-by: Julius Bairaktaris --- v2: - gate on a new hw flag, IEEE80211_HW_TX_NO_PUSHBACK, so a driver that pulls when the hardware has room is not affected. The flag is set by the driver rather than derived from the use of ieee80211_handle_wake_tx_queue(): a driver with its own callback can push everything it dequeues just the same, and ath11k does once it schedules its TXQs itself - add the ath11k patch that sets the flag as 2/2 - rewrite the message - v1: https://lore.kernel.org/linux-wireless/20260824142435.1757391-1-julius@bairaktaris.de/ Measured with the ath11k series "wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler" v4 applied, which advertises AQL and airtime fairness on ath11k, and with "wifi: mac80211: keep the TXQ scheduling round number across a closed round" applied. include/net/mac80211.h | 7 +++++++ net/mac80211/debugfs.c | 1 + net/mac80211/tx.c | 21 +++++++++++++++++++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/include/net/mac80211.h b/include/net/mac80211.h index 7bb4618065b6..80381c7abd84 100644 --- a/include/net/mac80211.h +++ b/include/net/mac80211.h @@ -3086,6 +3086,12 @@ struct ieee80211_txq { * @IEEE80211_HW_SUPPORTS_NDP_BLOCKACK: HW can transmit/receive S1G NDP * BlockAck frames. * + * @IEEE80211_HW_TX_NO_PUSHBACK: The driver hands every frame it dequeues + * to the hardware at once, and the hardware picks the station it serves + * next, so the order in which mac80211 schedules the queues does not + * reach the air. mac80211 then applies the airtime weight to the + * airtime queue limit instead. + * * @NUM_IEEE80211_HW_FLAGS: number of hardware flags, used for sizing arrays */ enum ieee80211_hw_flags { @@ -3147,6 +3153,7 @@ enum ieee80211_hw_flags { IEEE80211_HW_HANDLES_QUIET_CSA, IEEE80211_HW_STRICT, IEEE80211_HW_SUPPORTS_NDP_BLOCKACK, + IEEE80211_HW_TX_NO_PUSHBACK, /* keep last, obviously */ NUM_IEEE80211_HW_FLAGS diff --git a/net/mac80211/debugfs.c b/net/mac80211/debugfs.c index 8946b027782c..ddbfa991ec74 100644 --- a/net/mac80211/debugfs.c +++ b/net/mac80211/debugfs.c @@ -462,6 +462,7 @@ static const char *hw_flag_names[] = { FLAG(HANDLES_QUIET_CSA), FLAG(STRICT), FLAG(SUPPORTS_NDP_BLOCKACK), + FLAG(TX_NO_PUSHBACK), #undef FLAG }; diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c index c33092960df2..c94efff48d74 100644 --- a/net/mac80211/tx.c +++ b/net/mac80211/tx.c @@ -4217,6 +4217,21 @@ EXPORT_SYMBOL(__ieee80211_schedule_txq); DEFINE_STATIC_KEY_FALSE(aql_disable); +static u32 ieee80211_aql_sta_limit(struct ieee80211_local *local, + struct sta_info *sta, u32 limit) +{ + u32 scaled; + + if (!ieee80211_hw_check(&local->hw, TX_NO_PUSHBACK) || + sta->airtime_weight == IEEE80211_DEFAULT_AIRTIME_WEIGHT) + return limit; + + scaled = mult_frac(limit, sta->airtime_weight, + IEEE80211_DEFAULT_AIRTIME_WEIGHT); + + return min(scaled, max(limit, local->aql_threshold)); +} + bool ieee80211_txq_airtime_check(struct ieee80211_hw *hw, struct ieee80211_txq *txq) { @@ -4238,13 +4253,15 @@ bool ieee80211_txq_airtime_check(struct ieee80211_hw *hw, sta = container_of(txq->sta, struct sta_info, sta); if (atomic_read(&sta->airtime[txq->ac].aql_tx_pending) < - sta->airtime[txq->ac].aql_limit_low) + ieee80211_aql_sta_limit(local, sta, + sta->airtime[txq->ac].aql_limit_low)) return true; if (atomic_read(&local->aql_total_pending_airtime) < local->aql_threshold && atomic_read(&sta->airtime[txq->ac].aql_tx_pending) < - sta->airtime[txq->ac].aql_limit_high) + ieee80211_aql_sta_limit(local, sta, + sta->airtime[txq->ac].aql_limit_high)) return true; return false; -- 2.53.0