[PATCH ath-next v4 4/9] wifi: ath11k: report tx airtime and enable airtime fairness

From: Julius Bairaktaris

Date: Tue Sep 08 2026 - 10:08:36 EST


mac80211's airtime scheduler charges sta->airtime[ac].deficit from one
place, ieee80211_sta_register_airtime(). Unlike AQL there is no estimator
fallback, and ath11k never calls it, so the ATF feature bit cannot be
advertised on its own: the deficit would only ever be replenished, every
station would stay permanently in credit, and the DRR would be inert. The
bit is not a no-op in that state either, because it also moves txq
insertion to the head of the active list, which __ieee80211_schedule_txq()
documents as safe only because the DRR moves a station back once its
deficit goes negative.

fes_duration_us arrives with every PPDU's stats and is already
accumulated into arsta->tx_duration a few lines above. Register it, and
set the feature bit in the same change so the two cannot be separated.

The access category the airtime is charged to comes from the TID, and the
ack/BA status TLV that ath11k took it from is emitted only for a PPDU that
drew a response. A transmission that timed out, was filtered or was aborted
names its TID only in the completion and rate TLVs, both of which already
reach the driver and were read nowhere. Take the first of the three the
firmware supplied that names one of the sixteen QoS TIDs; a PPDU that names
none is charged to best effort, where ieee80211_sta_register_airtime()
also puts the management TID.

The call runs under ar->data_lock and ab->base_lock. mac80211 takes
local->active_txq_lock[ac] inside it and calls nothing back under that
lock, and ath10k registers from the same place under its data_lock.

Measured on an IPQ8074 AP with two stations on one radio, a 1x1 VHT
client and a 2x2 HE client, both receiving at once. Before, 'iw station
set <mac> airtime_weight' is refused with -EOPNOTSUPP and the per-station
airtime file reads zero. After, the weight is accepted and read back, TX
airtime accumulates, and the per-AC deficits move.

What the deficit can do with that is bounded by how the driver hands
frames to the hardware. Every station keeps its full airtime queue limit
in flight and the hardware picks among them, so the scheduling order
does not reach the air and a weight moves nothing on this driver by
itself. The bit is what makes the weight settable and the deficit real;
what acts on them is a mac80211 change posted separately, which scales
the limit by the weight for a driver that says it pushes everything.

fes_duration_us spans the frame exchange sequence, so it includes the
SIFS and the responding BlockAck, time the radio spends receiving; the
airtime registered is therefore larger than the airtime transmitted. It
is a per-PPDU field, so each user of a downlink MU PPDU is charged the
whole sequence, as arsta->tx_duration already accumulates it; the stats
carry no per-user duration. On the firmware tested here every PPDU
carried exactly one user, over 21051 PPDUs.

Tested-on: IPQ8074 hw2.0 AHB WLAN.HK.2.9.0.1-02146-QCAHKSWPL_SILICONZ-1

Assisted-by: Claude:claude-opus-5
Signed-off-by: Julius Bairaktaris <julius@xxxxxxxxxxxxxx>
---
drivers/net/wireless/ath/ath11k/dp_rx.c | 17 +++++++++++++++++
drivers/net/wireless/ath/ath11k/mac.c | 2 ++
2 files changed, 19 insertions(+)

diff --git a/drivers/net/wireless/ath/ath11k/dp_rx.c b/drivers/net/wireless/ath/ath11k/dp_rx.c
index 33425707c084..68472e1e748a 100644
--- a/drivers/net/wireless/ath/ath11k/dp_rx.c
+++ b/drivers/net/wireless/ath/ath11k/dp_rx.c
@@ -1377,6 +1377,7 @@ ath11k_update_per_peer_tx_stats(struct ath11k *ar,
u16 rate = 0, succ_pkts = 0;
u32 tx_duration = 0;
u8 tid = HTT_PPDU_STATS_NON_QOS_TID;
+ u8 airtime_tid;
bool is_ampdu = false;

if (!(usr_stats->tlv_flags & BIT(HTT_PPDU_STATS_TAG_USR_RATE)))
@@ -1485,6 +1486,22 @@ ath11k_update_per_peer_tx_stats(struct ath11k *ar,

arsta->txrate.bw = ath11k_mac_bw_to_mac80211_bw(bw);
arsta->tx_duration += tx_duration;
+
+ /* The ack/BA status TLV names the TID only for a PPDU that drew a
+ * response; fall back to the completion and rate TLVs, then to BE.
+ */
+ airtime_tid = tid;
+ if (airtime_tid >= IEEE80211_NUM_TIDS &&
+ usr_stats->tlv_flags & BIT(HTT_PPDU_STATS_TAG_USR_COMPLTN_COMMON))
+ airtime_tid = usr_stats->cmpltn_cmn.tid_num;
+ if (airtime_tid >= IEEE80211_NUM_TIDS)
+ airtime_tid = user_rate->tid_num;
+ if (airtime_tid >= IEEE80211_NUM_TIDS)
+ airtime_tid = 0;
+
+ if (tx_duration)
+ ieee80211_sta_register_airtime(sta, airtime_tid, tx_duration, 0);
+
memcpy(&arsta->last_txrate, &arsta->txrate, sizeof(struct rate_info));

/* PPDU stats reported for mgmt packet doesn't have valid tx bytes.
diff --git a/drivers/net/wireless/ath/ath11k/mac.c b/drivers/net/wireless/ath/ath11k/mac.c
index 38f641bbc53c..40a2b6d2f804 100644
--- a/drivers/net/wireless/ath/ath11k/mac.c
+++ b/drivers/net/wireless/ath/ath11k/mac.c
@@ -10593,6 +10593,8 @@ static int __ath11k_mac_register(struct ath11k *ar)

wiphy_ext_feature_set(ar->hw->wiphy, NL80211_EXT_FEATURE_CQM_RSSI_LIST);
wiphy_ext_feature_set(ar->hw->wiphy, NL80211_EXT_FEATURE_AQL);
+ wiphy_ext_feature_set(ar->hw->wiphy,
+ NL80211_EXT_FEATURE_AIRTIME_FAIRNESS);
wiphy_ext_feature_set(ar->hw->wiphy, NL80211_EXT_FEATURE_STA_TX_PWR);
if (test_bit(WMI_TLV_SERVICE_BSS_COLOR_OFFLOAD,
ar->ab->wmi_ab.svc_map)) {
--
2.53.0