Linux wireless drivers development
 help / color / mirror / Atom feed
* [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler
@ 2026-09-08 13:53 Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 1/9] wifi: ath11k: free tx skbs through ieee80211_free_txskb() Julius Bairaktaris
                   ` (8 more replies)
  0 siblings, 9 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

ath11k advertises neither NL80211_EXT_FEATURE_AQL nor
NL80211_EXT_FEATURE_AIRTIME_FAIRNESS. Every user of AQL in mac80211 is
gated on its bit, and the airtime scheduler's deficit is charged from
ieee80211_sta_register_airtime(), which ath11k never calls. Both
mechanisms are inert on this driver, and the driver keeps no queue of its
own, so on a saturated download every queued byte sits in the hardware TX
rings, where there is no AQM and no flow separation.

Measured on an IPQ8074 access point with a TCP download forwarded from a
wired host to one 1x1 VHT80 station, 20 s runs, three interleaved runs
per arm. The first column is the download rate, the second the RTT the
sender's TCP sees on that flow, mean and maximum over the run.

  stock                              101 Mbit/s   207-239 ms, max 313-322
  series, default limit              101 Mbit/s    48-55 ms, max 62-64
  series, BE aql_txq_limit 500/1000  101 Mbit/s    26-27 ms, max 31-32

With the access point's rate pinned to VHT MCS0, one stream, the same
frames are twenty times the airtime:

  stock                             23.6 Mbit/s   502-523 ms, max 871-1139
  series, default limit             23.3 Mbit/s    30-32 ms, max 38-41

On stock the download's own RTT is the depth of the hardware queue. With
the series the airtime queue limit bounds that depth, the station's aql
file reads 12024 us at the median against the 12000 us default high
limit and 1008 to 1024 us at 500/1000, and the rest of the backlog stays
in mac80211 where fq_codel manages it. A second flow to the same station,
a 20 Hz ping from the same wired host, reads 8 to 11 ms in every arm
including stock: ath11k spreads frames over three TCL rings by the
sending CPU, so a frame from another path lands in a ring the download
does not fill.

The 500/1000 setting is for a fast station. At MCS0 the station's aql
file reads 1308 us and the download drops to 9.3, 11.8 and 11.8 Mbit/s;
no default is changed, and where the limit sits is left to whoever
configures the access point.

mac80211 skips a txq in every other scheduling round once the access
category is over its airtime limit; "wifi: mac80211: keep the TXQ
scheduling round number across a closed round", posted separately, fixes
that. The downloads above do not depend on it, since the network stack's
arrivals start rounds often enough to cover the loss; a download
generated on the access point does, and reads 35.6 to 44.4 Mbit/s at
500/1000 without it against 95 with it. Everything here is measured
with it applied.

The airtime AQL charges is close to what the hardware reports. Comparing
what mac80211 charges against the radio's own measurement for the same
station, as pending airtime over outstanding MSDUs and as tx_duration
over completed MSDUs, the charge is 47.2, 50.9 and 50.4 us per MSDU
against 90.9, 92.6 and 91.4 measured, a ratio near 1.85. Part of that is
structural rather than estimator error: fes_duration_us spans the frame
exchange sequence including the responding BlockAck, while
ieee80211_rate_expected_tx_airtime() amortises the overhead over an
assumed aggregate. Patch 3 exposes the driver-side outstanding MSDU
count so the two can be compared at all.

With two stations on one radio, a 1x1 VHT client and a 2x2 HE client both
receiving at once, airtime_weight is refused with -EOPNOTSUPP on stock.
With the series and the limit binding, the HE client takes 52 to 60 per
cent of the airtime at every weight setting, nine runs, and the weight
does not move it: the scheduling order never reaches the air on this
driver.

Patch 1 is a prerequisite for patch 2 rather than an unrelated fix, and
the series should not be split on it: the tx completion path it corrects
is also the path that returns the AQL charge, so with the feature bit set
and that path still calling dev_kfree_skb_any(), pending airtime for an
access category rises without bound and no station on the radio is
scheduled in it again. It carries a Fixes tag and no stable Cc, since
the completion statuses it covers were not seen on the tested hardware.

Patch 2 advertises AQL. Patch 3 reports the outstanding MSDU count.
Patch 4 registers per-station transmit airtime and advertises airtime
fairness, which cannot be set without the reporting because the deficit
has no estimator fallback. Patch 5 moves the scheduling round into the
driver so a second context can drive it, patch 6 drives it from tx
completion, and patch 8 ends it when the hardware has no room. Patch 7
registers the hardware's per-PPDU receive duration into the same deficit.
Patch 9 bounds the tx completion handler to the NAPI budget, so that
the round patch 6 adds runs once per budget of completions rather than
once per ring. Patches 6 and 9 carry their own measurements.

Two notes on overlap and prior art.

"wifi: ath11k/ath12k: implement TX flow control" [1] also replaces
.wake_tx_queue in ath11k, to stop -ENOMEM under a full TCL ring. Its
handler serves only the TXQ it is handed and calls neither
ieee80211_txq_schedule_start() nor ieee80211_next_txq(), so it removes
the deficit round robin that ieee80211_handle_wake_tx_queue() performs
today and forecloses airtime fairness on this driver. This series keeps
that scheduling and adds the airtime accounting it needs; patch 8 handles
descriptor exhaustion by ending the round on -ENOSPC or -ENOMEM rather
than by reading the ring pointer once per frame. Both series replace the
same callback, so they conflict textually; if that series lands first,
this one will be rebased onto it and its ring check moved into the
scheduling round.

An AQL enable with a custom .wake_tx_queue and a tx-completion driven
push was proposed for ath11k in 2023 [2]. The design was right. What it
was asked for and did not have is the measurement above.

A related mac80211 change is posted separately, "wifi: mac80211: scale
the airtime queue limit by the station's weight" v2. On this driver
every station keeps its full airtime queue limit in flight and the
firmware picks the next transmitter among them, so the order
ieee80211_next_txq() produces never reaches the air and airtime_weight
has no effect on the split. That posting adds IEEE80211_HW_TX_NO_PUSHBACK,
scales the per-station limit by the weight for a driver that sets it,
and sets it in ath11k as its second patch. With this series, BE
aql_txq_limit 500/1000 us and that change, the HE client's share of the
airtime is 57.8, 57.5 and 55.1 per cent at equal weights, 73.8, 76.7 and
74.4 at 1024:256 and 22.6, 22.7 and 22.4 at 256:1024. The one-line
ath11k patch that sets the flag depends on the mac80211 patch and is
posted with it rather than here.

v4:
 - rebase onto ath-next, commit 1d8e73163ef9 ("wifi: ath11k: unregister
   PM notifier on QMI init failure path")
 - measure with a download forwarded from a wired host and report the
   download's own RTT. v3 measured a download generated on the access
   point, which TCP small queues bound before the hardware queue did,
   and probed from the station, which measured the station's uplink
 - a mac80211 fix posted separately, for a txq skipped every other
   scheduling round once the access category is full; v3 was measured
   with that bug present
 - patch 1: drop the stable Cc, say the statuses were not seen; move
   Tested-on above the tags as the rest of ath11k does
 - patch 2: drop the claim that the TIDs of one station test the limit
   independently; the pending counter is per access category
 - patch 3: the idr bounds the count added here, not the ring-full
   counter
 - patch 4: say why the lock nesting is safe and that each MU user is
   charged the whole PPDU as arsta->tx_duration already is
 - patch 6: start a round only for the access categories a reaped
   completion belonged to, and carry the with/without measurement in
   the message
 - patch 7: the default monitor status filter does not subscribe the
   user stats TLV that names the TID, so charge received airtime to BE
   unconditionally, and register it after ab->base_lock is dropped
 - patch 8: a full management queue also ends the round; the frame that
   met the shortage is still freed, and ath10k and mt76 avoid that by
   checking before they dequeue, which the message now says
 - patch 9: state what the budget does, which is to run the round of
   patch 6 once per budget of completions; the claim that receive rings
   wait behind the handler was wrong for IPQ8074, whose tx completion
   rings share no NAPI poll with them. Measured with and without
 - cover letter: the mac80211 weight change now gates on a hw flag the
   driver sets, the ath11k patch that sets it is posted with that
   change, and the airtime split is remeasured with it
 - v3: https://lore.kernel.org/all/20260824142350.1757379-1-julius@bairaktaris.de/

v3:
 - cover letter: v2 claimed the AQL estimate runs an order of magnitude
   under real occupancy. That was wrong; measured against the hardware's
   own tx_duration the ratio is 1.85, reported above
 - cover letter: v2 described the mac80211 weight change as posted
   separately while it was not; it is posted alongside this version
 - patch 1: state that the AQL half of the rationale needs the feature
   bit patch 2 sets, so the stable backport is not read as describing a
   stall that cannot occur without it
 - patch 2: correct "nothing restarts it until the next frame arrives
   from the network stack"; a round also starts on a block ack session
   change, a station leaving powersave, and a stopped queue restarting
 - patch 2: drop "cannot behave worse", which the same message's note
   about a stale rate contradicts
 - patch 2: say why pending airtime stands above the limit it is tested
   against
 - patch 3: the idr holds one entry short of DP_TX_IDR_SIZE
 - patch 4: drop the assertion about firmware TID markers that no ath11k
   header defines
 - patch 5: correct the claim that only a frame from the network stack
   can start a round, and the claim that mt76 relies on bottom halves
   being disabled; mt76 runs every round from one per-device tx worker
 - patch 5: state the bound conditionally. A selection ends where the
   airtime queue limit binds and drains the station's queue where it does
   not, which is what the generic handler does today
 - patch 6: the comment no longer says nothing else starts a round
 - patch 8: -ENOMEM also covers a DMA mapping failure
 - v2: https://lore.kernel.org/all/20260824074228.2114579-1-julius@bairaktaris.de/
 - v1: https://lore.kernel.org/all/20260823195703.49198-1-julius@bairaktaris.de/

[1] https://lore.kernel.org/all/20260811172435.616200-4-jtornosm@redhat.com/
[2] https://lore.kernel.org/all/20230501130725.7171-1-quic_tamizhr@quicinc.com/

Julius Bairaktaris (9):
  wifi: ath11k: free tx skbs through ieee80211_free_txskb()
  wifi: ath11k: enable airtime queue limits
  wifi: ath11k: report the pending tx MSDU count in soc_dp_stats
  wifi: ath11k: report tx airtime and enable airtime fairness
  wifi: ath11k: schedule TXQs from the driver
  wifi: ath11k: run the TXQ scheduler on tx completion
  wifi: ath11k: charge received airtime to the station deficit
  wifi: ath11k: stop a scheduling round when the hardware refuses a
    frame
  wifi: ath11k: budget the tx completion handler

 drivers/net/wireless/ath/ath11k/core.h    |  5 ++
 drivers/net/wireless/ath/ath11k/debugfs.c | 10 +++
 drivers/net/wireless/ath/ath11k/dp.c      | 10 ++-
 drivers/net/wireless/ath/ath11k/dp_rx.c   | 32 +++++++++
 drivers/net/wireless/ath/ath11k/dp_tx.c   | 28 ++++++--
 drivers/net/wireless/ath/ath11k/dp_tx.h   |  3 +-
 drivers/net/wireless/ath/ath11k/mac.c     | 81 +++++++++++++++++++++--
 drivers/net/wireless/ath/ath11k/mac.h     |  1 +
 8 files changed, 157 insertions(+), 13 deletions(-)

-- 
2.53.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 1/9] wifi: ath11k: free tx skbs through ieee80211_free_txskb()
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 2/9] wifi: ath11k: enable airtime queue limits Julius Bairaktaris
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

ath11k_dp_tx_free_txbuf(), reached from the HTT tx completion handler for
the REINJ and INSPECT statuses, releases the frame with
dev_kfree_skb_any(). Every other tx completion path in dp_tx.c hands the
skb back to mac80211, and this one is the last that does not.

A frame freed that way keeps whatever state mac80211 was holding for it.
A data frame whose socket asked for tx status has a clone parked in
local->ack_status_frames and an index allocated from an idr bounded at
8191 entries; ieee80211_free_txskb() releases both through
ieee80211_report_used_skb(), and dev_kfree_skb_any() leaks them for the
lifetime of the hw. Commit 29d15589f084 ("wifi: ath11k: Cleanup mac80211
references on failure during tx_complete") converted three sibling paths
for exactly that reason and left this one.

On a driver that advertises NL80211_EXT_FEATURE_AQL, which the next
patch makes ath11k do, ieee80211_free_txskb() also returns the pending
airtime the frame was charged at dequeue, and dev_kfree_skb_any() does
not. The charge then stays on the access category for good, and once
local->aql_ac_pending_airtime[] reaches its limit
ieee80211_txq_schedule_airtime_check() stops every station on the radio
in that category.

Neither status was seen on the hardware below during the measurements
in this series, so the change is by inspection of the completion path.

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

Fixes: d5c65159f289 ("ath11k: driver for Qualcomm IEEE 802.11ax devices")
Assisted-by: Claude:claude-opus-5
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
---
 drivers/net/wireless/ath/ath11k/dp_tx.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/drivers/net/wireless/ath/ath11k/dp_tx.c b/drivers/net/wireless/ath/ath11k/dp_tx.c
index cac970c92806..52c4a9924a31 100644
--- a/drivers/net/wireless/ath/ath11k/dp_tx.c
+++ b/drivers/net/wireless/ath/ath11k/dp_tx.c
@@ -305,9 +305,10 @@ static void ath11k_dp_tx_free_txbuf(struct ath11k_base *ab, u8 mac_id,
 	skb_cb = ATH11K_SKB_CB(msdu);
 
 	dma_unmap_single(ab->dev, skb_cb->paddr, msdu->len, DMA_TO_DEVICE);
-	dev_kfree_skb_any(msdu);
 
 	ar = ab->pdevs[mac_id].ar;
+	ieee80211_free_txskb(ar->hw, msdu);
+
 	if (atomic_dec_and_test(&ar->dp.num_tx_pending))
 		wake_up(&ar->dp.tx_empty_waitq);
 }
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 2/9] wifi: ath11k: enable airtime queue limits
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 1/9] wifi: ath11k: free tx skbs through ieee80211_free_txskb() Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 3/9] wifi: ath11k: report the pending tx MSDU count in soc_dp_stats Julius Bairaktaris
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

ath11k does not advertise NL80211_EXT_FEATURE_AQL, and every user of AQL in
mac80211 is gated on that bit: the airtime charge in
ieee80211_tx_dequeue(), ieee80211_txq_airtime_check(),
ieee80211_sta_update_pending_airtime(), and the per-station aql file in
debugfs, which is not even created.

That matters here more than it would elsewhere, because AQL is the only
brake. ath11k uses ieee80211_handle_wake_tx_queue(), whose
wake_tx_push_queue() drains a selected txq whole into the driver, and the
check at the top of ieee80211_tx_dequeue() is what stops that drain.
Without the feature bit nothing does, so mac80211 holds no backlog and
every queued byte of a saturated download sits in the hardware TX rings,
where there is no AQM and no flow separation.

Measured on an IPQ8074 AP with a TCP download forwarded from a wired
host to one 1x1 VHT80 station at 101 Mbit/s, sampled once a second from
that station's own aql file: BE pending airtime sits at the 12000 us
default high limit, 12024 us at the median, and at 1008 to 1024 us when
the limit is lowered to 500/1000 us. Pending airtime stands a frame
above the limit it is tested against because
ieee80211_txq_airtime_check() admits a frame while pending is still
below it. Without this patch the file does not exist and the limits are
never consulted.

The rate the estimator needs is already there: ath11k keeps a per-station
rate_info in arsta->last_txrate and passes it to ieee80211_tx_status_ext(),
which stores it as tx_stats.last_rate_info, and that is what
ieee80211_calc_expected_tx_airtime() reads. Where no rate is known yet the
estimator falls back to its 4 us floor, so a station's queue is charged the
minimum until the first HTT PPDU stats arrive and the per-AC limits of 5000
and 12000 us stay loose over that window. The current code charges nothing
and consults no limit at all, so that window changes nothing. A rate that
has gone stale low overcharges a station that has since sped up, bounded
by the same lag.

The estimate is only as fresh as arsta->last_txrate, which is updated from
the HTT PPDU stats when a descriptor is evicted from ar->ppdu_stats_info,
so the rate behind it can lag a rate change by up to
HTT_PPDU_DESC_MAX_DEPTH PPDUs.

ieee80211_sta_update_pending_airtime() returns the airtime but does not
reschedule the txq, and ath11k has no completion-side push, so a queue held
back by AQL is re-poked only by the next enqueue or queue wake.

The bit alone brakes the queue but does not refill it: the check at the top
of ieee80211_tx_dequeue() stops the drain, and nothing restarts it until
mac80211 next calls the wake_tx_queue callback. Later patches in this
series give the driver a scheduling round and run it from tx completion,
which is what makes a binding limit affordable.

ath10k and mt76 set the same feature bit.

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@bairaktaris.de>
---
 drivers/net/wireless/ath/ath11k/mac.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/drivers/net/wireless/ath/ath11k/mac.c b/drivers/net/wireless/ath/ath11k/mac.c
index 2d55cdc4d165..38f641bbc53c 100644
--- a/drivers/net/wireless/ath/ath11k/mac.c
+++ b/drivers/net/wireless/ath/ath11k/mac.c
@@ -10592,6 +10592,7 @@ static int __ath11k_mac_register(struct ath11k *ar)
 	ar->hw->sta_data_size = sizeof(struct ath11k_sta);
 
 	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_STA_TX_PWR);
 	if (test_bit(WMI_TLV_SERVICE_BSS_COLOR_OFFLOAD,
 		     ar->ab->wmi_ab.svc_map)) {
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 3/9] wifi: ath11k: report the pending tx MSDU count in soc_dp_stats
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 1/9] wifi: ath11k: free tx skbs through ieee80211_free_txskb() Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 2/9] wifi: ath11k: enable airtime queue limits Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 4/9] wifi: ath11k: report tx airtime and enable airtime fairness Julius Bairaktaris
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

ar->dp.num_tx_pending counts the MSDUs the hardware has not completed
yet, and is used to wait for the transmit path to drain. It is never
shown.

soc_dp_stats can therefore say when the transmit path overflowed - TCL ring
full, misc transmit failures - and not how deep it is at this instant. The
count added here is bounded by the tx descriptor idr, one entry short of
DP_TX_IDR_SIZE per ring, which is large enough that occupancy rather than
exhaustion is what characterises the path under load.

Print it per radio, beside the failure counters it complements.

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@bairaktaris.de>
---
 drivers/net/wireless/ath/ath11k/debugfs.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/drivers/net/wireless/ath/ath11k/debugfs.c b/drivers/net/wireless/ath/ath11k/debugfs.c
index 70922612d3fb..ffe1f466aefb 100644
--- a/drivers/net/wireless/ath/ath11k/debugfs.c
+++ b/drivers/net/wireless/ath/ath11k/debugfs.c
@@ -714,6 +714,16 @@ static ssize_t ath11k_debugfs_dump_soc_dp_stats(struct file *file,
 			 "\nMisc Transmit Failures: %d\n",
 			 atomic_read(&soc_stats->tx_err.misc_fail));
 
+	/* Descriptors the hardware has not completed. */
+	len += scnprintf(buf + len, size - len, "\nPending Tx MSDUs:\n");
+	for (i = 0; i < ab->num_radios; i++) {
+		struct ath11k *ar = ab->pdevs[i].ar;
+
+		if (ar)
+			len += scnprintf(buf + len, size - len, "radio%d: %d\n",
+					 i, atomic_read(&ar->dp.num_tx_pending));
+	}
+
 	len += ath11k_debugfs_dump_soc_ring_bp_stats(ab, buf + len, size - len);
 
 	if (len > size)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 4/9] wifi: ath11k: report tx airtime and enable airtime fairness
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
                   ` (2 preceding siblings ...)
  2026-09-08 13:53 ` [PATCH ath-next v4 3/9] wifi: ath11k: report the pending tx MSDU count in soc_dp_stats Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 5/9] wifi: ath11k: schedule TXQs from the driver Julius Bairaktaris
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

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@bairaktaris.de>
---
 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


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 5/9] wifi: ath11k: schedule TXQs from the driver
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
                   ` (3 preceding siblings ...)
  2026-09-08 13:53 ` [PATCH ath-next v4 4/9] wifi: ath11k: report tx airtime and enable airtime fairness Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 6/9] wifi: ath11k: run the TXQ scheduler on tx completion Julius Bairaktaris
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

ieee80211_handle_wake_tx_queue() has exactly one entry point, the
wake_tx_queue callback, so a round starts only where mac80211 calls that
callback: a frame arriving from the network stack, a block ack session
starting or stopping, a station leaving powersave, or a stopped hardware
queue restarting. Every round runs under
local->handle_wake_tx_queue_lock, which is shared by all four access
categories of the hardware.

That is the wrong shape for a driver whose only backpressure is the
airtime queue limit. When ieee80211_txq_airtime_check() refuses a
station, ieee80211_tx_dequeue() stops handing out frames and the
station's intermediate queue keeps its backlog until the next enqueue,
not until the airtime it is waiting on comes back from the hardware.
Returning that airtime does not reschedule anything.

Open-code the round in the driver so that a second context can drive it.
The walk is the same deficit round-robin over the active TXQs of one
access category that the generic handler performs. A selection ends
where the airtime queue limit binds, because ieee80211_tx_dequeue()
applies the limit itself, and drains the station's queue where it does
not, which is what the generic handler does today.

mac80211 documents that a driver must not run concurrent scheduling rounds,
and the next patch adds a second context that starts one. The round
therefore takes a per-category lock rather than the hardware-wide one.
ath10k and mt76 also drive the round from the driver; ath10k takes its
per-category lock on the wake path alone, and mt76 serialises instead by
running its rounds from one per-device tx worker.

A custom .wake_tx_queue with a tx-completion driven push was proposed for
ath11k once before [1]; the design was right and the measurement that
justifies it is in the next patch.

Link: https://lore.kernel.org/all/20230501130725.7171-1-quic_tamizhr@quicinc.com/ [1]

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@bairaktaris.de>
---
 drivers/net/wireless/ath/ath11k/core.h |  5 +++
 drivers/net/wireless/ath/ath11k/mac.c  | 44 ++++++++++++++++++++++++--
 2 files changed, 47 insertions(+), 2 deletions(-)

diff --git a/drivers/net/wireless/ath/ath11k/core.h b/drivers/net/wireless/ath/ath11k/core.h
index a0d725923ef2..40895d276de5 100644
--- a/drivers/net/wireless/ath/ath11k/core.h
+++ b/drivers/net/wireless/ath/ath11k/core.h
@@ -709,6 +709,11 @@ struct ath11k {
 	 */
 	spinlock_t data_lock;
 
+	/* serialises one scheduling round per access category against the
+	 * other contexts that drive it
+	 */
+	spinlock_t txq_lock[IEEE80211_NUM_ACS];
+
 	struct list_head arvifs;
 	/* should never be NULL; needed for regular htt rx */
 	struct ieee80211_channel *rx_channel;
diff --git a/drivers/net/wireless/ath/ath11k/mac.c b/drivers/net/wireless/ath/ath11k/mac.c
index 40a2b6d2f804..342b38308468 100644
--- a/drivers/net/wireless/ath/ath11k/mac.c
+++ b/drivers/net/wireless/ath/ath11k/mac.c
@@ -6546,6 +6546,43 @@ static void ath11k_mac_op_tx(struct ieee80211_hw *hw,
 	}
 }
 
+static void ath11k_mac_tx_push_txq(struct ath11k *ar, struct ieee80211_txq *txq)
+{
+	struct ieee80211_tx_control control = { .sta = txq->sta };
+	struct sk_buff *skb;
+
+	/* ieee80211_tx_dequeue() applies the airtime queue limit itself, so a
+	 * selection ends where that limit binds, where the hardware queue is
+	 * stopped, or when the queue empties, as the generic handler does
+	 * today.
+	 */
+	while ((skb = ieee80211_tx_dequeue(ar->hw, txq)))
+		ath11k_mac_op_tx(ar->hw, &control, skb);
+}
+
+static void ath11k_mac_schedule_txq(struct ath11k *ar, u8 ac)
+{
+	struct ieee80211_hw *hw = ar->hw;
+	struct ieee80211_txq *txq;
+
+	spin_lock_bh(&ar->txq_lock[ac]);
+
+	ieee80211_txq_schedule_start(hw, ac);
+	while ((txq = ieee80211_next_txq(hw, ac))) {
+		ath11k_mac_tx_push_txq(ar, txq);
+		ieee80211_return_txq(hw, txq, false);
+	}
+	ieee80211_txq_schedule_end(hw, ac);
+
+	spin_unlock_bh(&ar->txq_lock[ac]);
+}
+
+static void ath11k_mac_op_wake_tx_queue(struct ieee80211_hw *hw,
+					struct ieee80211_txq *txq)
+{
+	ath11k_mac_schedule_txq(hw->priv, txq->ac);
+}
+
 void ath11k_mac_drain_tx(struct ath11k *ar)
 {
 	/* make sure rcu-protected mac80211 tx path itself is drained */
@@ -10067,7 +10104,7 @@ static int ath11k_mac_op_sta_state(struct ieee80211_hw *hw,
 
 static const struct ieee80211_ops ath11k_ops = {
 	.tx				= ath11k_mac_op_tx,
-	.wake_tx_queue			= ieee80211_handle_wake_tx_queue,
+	.wake_tx_queue			= ath11k_mac_op_wake_tx_queue,
 	.start                          = ath11k_mac_op_start,
 	.stop                           = ath11k_mac_op_stop,
 	.reconfig_complete              = ath11k_mac_op_reconfig_complete,
@@ -10757,7 +10794,7 @@ int ath11k_mac_allocate(struct ath11k_base *ab)
 	struct ath11k *ar;
 	struct ath11k_pdev *pdev;
 	int ret;
-	int i;
+	int i, j;
 
 	if (test_bit(ATH11K_FLAG_REGISTERED, &ab->dev_flags))
 		return 0;
@@ -10791,6 +10828,9 @@ int ath11k_mac_allocate(struct ath11k_base *ab)
 
 		pdev->ar = ar;
 		spin_lock_init(&ar->data_lock);
+
+		for (j = 0; j < IEEE80211_NUM_ACS; j++)
+			spin_lock_init(&ar->txq_lock[j]);
 		INIT_LIST_HEAD(&ar->arvifs);
 		INIT_LIST_HEAD(&ar->ppdu_stats_info);
 		mutex_init(&ar->conf_mutex);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 6/9] wifi: ath11k: run the TXQ scheduler on tx completion
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
                   ` (4 preceding siblings ...)
  2026-09-08 13:53 ` [PATCH ath-next v4 5/9] wifi: ath11k: schedule TXQs from the driver Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 7/9] wifi: ath11k: charge received airtime to the station deficit Julius Bairaktaris
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

A station whose airtime queue limit is reached stops being served by
ieee80211_tx_dequeue(), and the airtime it is waiting on is only returned
when the hardware completes the frames already charged to it. ath11k starts
a scheduling round only where mac80211 calls the wake_tx_queue callback, so
once the limit binds, the backlog moves again only when one of those events
comes round: most often the next frame from the network stack. Under a
saturated download the arrivals keep the round running; a flow that has
just filled the intermediate queue, or one whose sender is waiting on the
acknowledgements behind that queue, stalls for as long as it takes the next
arrival to appear.

Start a round for each access category a reaped completion belonged to,
on that radio, at the point where the airtime has been returned. The
airtime queue limit then bounds a queue that is refilled as it drains,
rather than one that is refilled when the network stack happens to push.
The access category is the queue mapping mac80211 stamped on the frame.

Measured on an IPQ8074 AP with a TCP download forwarded from a wired
host to one 1x1 VHT80 station, BE aql_txq_limit 500/1000 us, 20 s runs,
three interleaved runs per arm, download rate and the RTT the sender's
TCP sees on that flow, mean and maximum:

                    Mbit/s               RTT mean ms   RTT max ms
  with this patch   100.8, 101.5, 100.6  27, 26, 26    32, 31, 32
  without            93.0,  99.1,  76.2  26, 27, 94    66, 36, 347

Without it the download moves only when the network stack pushes, and
a run in three stalls until the sender retransmits. At the default
limit, which binds on this flow as well, this patch reads 101 Mbit/s at
48 to 55 ms and the arm without it 80.7 to 101.1 Mbit/s at 54 to 79 ms.

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@bairaktaris.de>
---
 drivers/net/wireless/ath/ath11k/dp_tx.c | 14 +++++++++++++-
 drivers/net/wireless/ath/ath11k/mac.c   |  7 +++++++
 drivers/net/wireless/ath/ath11k/mac.h   |  1 +
 3 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/drivers/net/wireless/ath/ath11k/dp_tx.c b/drivers/net/wireless/ath/ath11k/dp_tx.c
index 52c4a9924a31..70d326fe6a1f 100644
--- a/drivers/net/wireless/ath/ath11k/dp_tx.c
+++ b/drivers/net/wireless/ath/ath11k/dp_tx.c
@@ -694,9 +694,10 @@ void ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id)
 	struct sk_buff *msdu;
 	struct hal_tx_status ts = {};
 	struct dp_tx_ring *tx_ring = &dp->tx_ring[ring_id];
+	unsigned long push = 0;
 	u32 *desc;
 	u32 msdu_id;
-	u8 mac_id;
+	u8 mac_id, i;
 
 	spin_lock_bh(&status_ring->lock);
 
@@ -756,12 +757,23 @@ void ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id)
 		spin_unlock(&tx_ring->tx_idr_lock);
 
 		ar = ab->pdevs[mac_id].ar;
+		__set_bit(mac_id * IEEE80211_NUM_ACS + skb_get_queue_mapping(msdu),
+			  &push);
 
 		if (atomic_dec_and_test(&ar->dp.num_tx_pending))
 			wake_up(&ar->dp.tx_empty_waitq);
 
 		ath11k_dp_tx_complete_msdu(ar, msdu, &ts);
 	}
+
+	/* A completion returns the airtime the frame was charged, which is
+	 * what an airtime-limited station in that access category is waiting
+	 * on. The arrivals that would otherwise start a round are themselves
+	 * queued behind that limit.
+	 */
+	for_each_set_bit(i, &push, ab->num_radios * IEEE80211_NUM_ACS)
+		ath11k_mac_tx_push_pending(ab->pdevs[i / IEEE80211_NUM_ACS].ar,
+					   i % IEEE80211_NUM_ACS);
 }
 
 int ath11k_dp_tx_send_reo_cmd(struct ath11k_base *ab, struct dp_rx_tid *rx_tid,
diff --git a/drivers/net/wireless/ath/ath11k/mac.c b/drivers/net/wireless/ath/ath11k/mac.c
index 342b38308468..ffb6b865c2df 100644
--- a/drivers/net/wireless/ath/ath11k/mac.c
+++ b/drivers/net/wireless/ath/ath11k/mac.c
@@ -6583,6 +6583,13 @@ static void ath11k_mac_op_wake_tx_queue(struct ieee80211_hw *hw,
 	ath11k_mac_schedule_txq(hw->priv, txq->ac);
 }
 
+void ath11k_mac_tx_push_pending(struct ath11k *ar, u8 ac)
+{
+	rcu_read_lock();
+	ath11k_mac_schedule_txq(ar, ac);
+	rcu_read_unlock();
+}
+
 void ath11k_mac_drain_tx(struct ath11k *ar)
 {
 	/* make sure rcu-protected mac80211 tx path itself is drained */
diff --git a/drivers/net/wireless/ath/ath11k/mac.h b/drivers/net/wireless/ath/ath11k/mac.h
index 59f83c7175fd..d577c5ca249c 100644
--- a/drivers/net/wireless/ath/ath11k/mac.h
+++ b/drivers/net/wireless/ath/ath11k/mac.h
@@ -161,6 +161,7 @@ struct ath11k *ath11k_mac_get_ar_by_vdev_id(struct ath11k_base *ab, u32 vdev_id)
 struct ath11k *ath11k_mac_get_ar_by_pdev_id(struct ath11k_base *ab, u32 pdev_id);
 
 void ath11k_mac_drain_tx(struct ath11k *ar);
+void ath11k_mac_tx_push_pending(struct ath11k *ar, u8 ac);
 void ath11k_mac_peer_cleanup_all(struct ath11k *ar);
 int ath11k_mac_tx_mgmt_pending_free(int buf_id, void *skb, void *ctx);
 u8 ath11k_mac_bw_to_mac80211_bw(u8 bw);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 7/9] wifi: ath11k: charge received airtime to the station deficit
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
                   ` (5 preceding siblings ...)
  2026-09-08 13:53 ` [PATCH ath-next v4 6/9] wifi: ath11k: run the TXQ scheduler on tx completion Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 8/9] wifi: ath11k: stop a scheduling round when the hardware refuses a frame Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 9/9] wifi: ath11k: budget the tx completion handler Julius Bairaktaris
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

mac80211's airtime scheduler is configured to account both directions:
local->airtime_flags is AIRTIME_USE_TX | AIRTIME_USE_RX, and
ieee80211_sta_register_airtime() adds the received airtime to the same
deficit the transmit side subtracts from. ath11k passes zero for it, so
an access point serving a station that mostly uploads charges that
station nothing for the medium it occupies, and the deficit that decides
whose queue is served next describes half the traffic.

Register the duration the hardware reports for each received PPDU. It
arrives in the RXPCU PPDU end TLV on the monitor status ring, which is
subscribed for every radio, and ath11k already accumulates it as the
station's rx_duration. Charging it once per PPDU matches what the
transmit side registers, whereas an estimate rebuilt per MSDU counts the
preamble and the interframe space once for every subframe of an
aggregate.

The PPDU end user stats TLV that names the TID is not in the default
monitor status filter, so the airtime is charged to best effort, the
access category the deficit arbitrates the bulk of the traffic in. The
call is made after ab->base_lock is dropped, under the rcu read lock the
loop holds, as the tx completion path does; base_lock is contended by
every tx completion. A PPDU from a peer with no station entry is not
charged to anyone.

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@bairaktaris.de>
---
 drivers/net/wireless/ath/ath11k/dp_rx.c | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/drivers/net/wireless/ath/ath11k/dp_rx.c b/drivers/net/wireless/ath/ath11k/dp_rx.c
index 68472e1e748a..a7b9bb295728 100644
--- a/drivers/net/wireless/ath/ath11k/dp_rx.c
+++ b/drivers/net/wireless/ath/ath11k/dp_rx.c
@@ -5270,6 +5270,7 @@ int ath11k_dp_rx_process_mon_status(struct ath11k_base *ab, int mac_id,
 	struct sk_buff_head skb_list;
 	struct ath11k_peer *peer;
 	struct ath11k_sta *arsta;
+	struct ieee80211_sta *sta;
 	int num_buffs_reaped = 0;
 	u32 rx_buf_sz;
 	u16 log_type;
@@ -5324,6 +5325,7 @@ int ath11k_dp_rx_process_mon_status(struct ath11k_base *ab, int mac_id,
 			continue;
 		}
 
+		sta = NULL;
 		rcu_read_lock();
 		spin_lock_bh(&ab->base_lock);
 		peer = ath11k_peer_find_by_id(ab, ppdu_info->peer_id);
@@ -5337,12 +5339,25 @@ int ath11k_dp_rx_process_mon_status(struct ath11k_base *ab, int mac_id,
 
 		arsta = ath11k_sta_to_arsta(peer->sta);
 		ath11k_dp_rx_update_peer_stats(arsta, ppdu_info);
+		sta = peer->sta;
 
 		if (ath11k_debugfs_is_pktlog_peer_valid(ar, peer->addr))
 			trace_ath11k_htt_rxdesc(ar, skb->data, log_type, rx_buf_sz);
 
 next_skb:
 		spin_unlock_bh(&ab->base_lock);
+
+		/* The airtime a station spends transmitting is taken from
+		 * the others, so its deficit is charged for it too, with the
+		 * duration the hardware measured for the whole PPDU, as on
+		 * the transmit side. The TLV that names the TID is not in
+		 * the default monitor status filter, so the charge goes to
+		 * BE. Outside base_lock, which tx completion contends for;
+		 * sta is held by the rcu read lock.
+		 */
+		if (sta && ppdu_info->rx_duration)
+			ieee80211_sta_register_airtime(sta, 0, 0,
+						       ppdu_info->rx_duration);
 		rcu_read_unlock();
 
 		dev_kfree_skb_any(skb);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 8/9] wifi: ath11k: stop a scheduling round when the hardware refuses a frame
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
                   ` (6 preceding siblings ...)
  2026-09-08 13:53 ` [PATCH ath-next v4 7/9] wifi: ath11k: charge received airtime to the station deficit Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  2026-09-08 13:53 ` [PATCH ath-next v4 9/9] wifi: ath11k: budget the tx completion handler Julius Bairaktaris
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

ath11k_dp_tx() returns -ENOSPC when the MSDU idr is exhausted and -ENOMEM
when no TCL descriptor is free on any ring or a DMA mapping fails, and the
caller answers both by freeing the frame. Inside a scheduling round that is
a loop: the next iteration pulls the next frame out of the same flow queue
and drops that one too, so a transient shortage costs the head of an
FQ-CoDel queue rather than the tail of a hardware one.

Return the error from the transmit path and end the round on it. The
frame that met the shortage is freed as before; the ones behind it stay
queued. The round-robin over the remaining stations stops as well, since
a shortage that reaches this point is not specific to the station being
served.

ath11k_mac_mgmt_tx() returns -ENOSPC as well, for a full management
queue, and that ends the round the same way; the next tx completion on
the radio starts another.

ath11k_dp_tx() also rejects a frame the hardware can never accept, with
-EINVAL or -EOPNOTSUPP, and every frame with -ESHUTDOWN while a firmware
crash is being flushed; those describe the frame or the device and not the
ring, so the round continues past them.

ath10k and mt76 check for room before they dequeue and never hold a frame
at this point; ath11k learns of the shortage from ath11k_dp_tx() and
cannot.

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@bairaktaris.de>
---
 drivers/net/wireless/ath/ath11k/mac.c | 41 ++++++++++++++++++++-------
 1 file changed, 30 insertions(+), 11 deletions(-)

diff --git a/drivers/net/wireless/ath/ath11k/mac.c b/drivers/net/wireless/ath/ath11k/mac.c
index ffb6b865c2df..12535926631b 100644
--- a/drivers/net/wireless/ath/ath11k/mac.c
+++ b/drivers/net/wireless/ath/ath11k/mac.c
@@ -6499,9 +6499,9 @@ static int ath11k_mac_mgmt_tx(struct ath11k *ar, struct sk_buff *skb,
 	return 0;
 }
 
-static void ath11k_mac_op_tx(struct ieee80211_hw *hw,
-			     struct ieee80211_tx_control *control,
-			     struct sk_buff *skb)
+static int ath11k_mac_tx(struct ieee80211_hw *hw,
+			 struct ieee80211_tx_control *control,
+			 struct sk_buff *skb)
 {
 	struct ath11k_skb_cb *skb_cb = ATH11K_SKB_CB(skb);
 	struct ath11k *ar = hw->priv;
@@ -6533,7 +6533,7 @@ static void ath11k_mac_op_tx(struct ieee80211_hw *hw,
 				    ret);
 			ieee80211_free_txskb(ar->hw, skb);
 		}
-		return;
+		return ret;
 	}
 
 	if (control->sta)
@@ -6544,20 +6544,36 @@ static void ath11k_mac_op_tx(struct ieee80211_hw *hw,
 		ath11k_warn(ar->ab, "failed to transmit frame %d\n", ret);
 		ieee80211_free_txskb(ar->hw, skb);
 	}
+
+	return ret;
+}
+
+static void ath11k_mac_op_tx(struct ieee80211_hw *hw,
+			     struct ieee80211_tx_control *control,
+			     struct sk_buff *skb)
+{
+	ath11k_mac_tx(hw, control, skb);
 }
 
-static void ath11k_mac_tx_push_txq(struct ath11k *ar, struct ieee80211_txq *txq)
+static int ath11k_mac_tx_push_txq(struct ath11k *ar, struct ieee80211_txq *txq)
 {
 	struct ieee80211_tx_control control = { .sta = txq->sta };
 	struct sk_buff *skb;
+	int ret;
 
 	/* ieee80211_tx_dequeue() applies the airtime queue limit itself, so a
-	 * selection ends where that limit binds, where the hardware queue is
-	 * stopped, or when the queue empties, as the generic handler does
-	 * today.
+	 * selection ends where the airtime queue limit binds, where the
+	 * hardware queue is stopped, or when the queue empties, as the generic
+	 * handler does today, and additionally when the hardware refuses a
+	 * frame.
 	 */
-	while ((skb = ieee80211_tx_dequeue(ar->hw, txq)))
-		ath11k_mac_op_tx(ar->hw, &control, skb);
+	while ((skb = ieee80211_tx_dequeue(ar->hw, txq))) {
+		ret = ath11k_mac_tx(ar->hw, &control, skb);
+		if (unlikely(ret == -ENOSPC || ret == -ENOMEM))
+			return ret;
+	}
+
+	return 0;
 }
 
 static void ath11k_mac_schedule_txq(struct ath11k *ar, u8 ac)
@@ -6569,8 +6585,11 @@ static void ath11k_mac_schedule_txq(struct ath11k *ar, u8 ac)
 
 	ieee80211_txq_schedule_start(hw, ac);
 	while ((txq = ieee80211_next_txq(hw, ac))) {
-		ath11k_mac_tx_push_txq(ar, txq);
+		int ret = ath11k_mac_tx_push_txq(ar, txq);
+
 		ieee80211_return_txq(hw, txq, false);
+		if (unlikely(ret))
+			break;
 	}
 	ieee80211_txq_schedule_end(hw, ac);
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH ath-next v4 9/9] wifi: ath11k: budget the tx completion handler
  2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
                   ` (7 preceding siblings ...)
  2026-09-08 13:53 ` [PATCH ath-next v4 8/9] wifi: ath11k: stop a scheduling round when the hardware refuses a frame Julius Bairaktaris
@ 2026-09-08 13:53 ` Julius Bairaktaris
  8 siblings, 0 replies; 10+ messages in thread
From: Julius Bairaktaris @ 2026-09-08 13:53 UTC (permalink / raw)
  To: jjohnson
  Cc: linux-wireless, ath11k, linux-kernel, toke, sven, quic_vthiagar,
	john

ath11k_dp_tx_completion_handler() reaps every completion the release
ring holds before it returns, and ath11k_dp_service_srng() charges none
of that work against the NAPI budget the poll was given, unlike every
receive handler below it. With the scheduling round that now ends the
handler, one poll that finds the ring full runs one round for all of it.

Reap at most the budget's worth of completions per poll and return the
count, so the poll ends and the round runs once per budget of
completions rather than once per ring. The completions left in the
FIFO are reaped by the next poll, which NAPI schedules at once when the
budget was spent.

Measured on an IPQ8074 AP with a TCP download forwarded from a wired
host to one 1x1 VHT80 station, BE aql_txq_limit 500/1000 us, 20 s runs,
three interleaved runs per arm, download rate and the RTT the sender's
TCP sees on that flow, mean and maximum:

                    Mbit/s               RTT mean ms   RTT max ms
  with this patch   100.8, 101.5, 100.6   27, 26, 26   32, 31, 32
  without            74.7, 101.1, 100.6  120, 27, 26   1873, 31, 30

Without it a poll that finds the ring full reaps all of it before the
round runs, and one run in three stalls for close to two seconds. At
the default limit both arms read 101 Mbit/s at 47 to 55 ms. Two soaks
on the series, a 91 s TCP soak at 1033 Mbit/s aggregate over five
streams, about 86000 completions per second, and a 60 s soak of
200-byte UDP datagrams at 60906 packets per second, produced no
status_fifo-is-full warning and no TCL ring-full failure.

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@bairaktaris.de>
---
 drivers/net/wireless/ath/ath11k/dp.c    | 10 ++++++++--
 drivers/net/wireless/ath/ath11k/dp_tx.c | 11 +++++++++--
 drivers/net/wireless/ath/ath11k/dp_tx.h |  3 ++-
 3 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/drivers/net/wireless/ath/ath11k/dp.c b/drivers/net/wireless/ath/ath11k/dp.c
index be65d73b904b..ddb434c513e0 100644
--- a/drivers/net/wireless/ath/ath11k/dp.c
+++ b/drivers/net/wireless/ath/ath11k/dp.c
@@ -783,8 +783,14 @@ int ath11k_dp_service_srng(struct ath11k_base *ab,
 
 	for (i = 0; i < ab->hw_params.hal_params->num_tx_rings; i++) {
 		if (BIT(ab->hw_params.hal_params->tcl2wbm_rbm_map[i].wbm_ring_num) &
-		    ab->hw_params.ring_mask->tx[grp_id])
-			ath11k_dp_tx_completion_handler(ab, i);
+		    ab->hw_params.ring_mask->tx[grp_id]) {
+			work_done =
+				ath11k_dp_tx_completion_handler(ab, i, budget);
+			budget -= work_done;
+			tot_work_done += work_done;
+			if (budget <= 0)
+				goto done;
+		}
 	}
 
 	if (ab->hw_params.ring_mask->rx_err[grp_id]) {
diff --git a/drivers/net/wireless/ath/ath11k/dp_tx.c b/drivers/net/wireless/ath/ath11k/dp_tx.c
index 70d326fe6a1f..3997bd336942 100644
--- a/drivers/net/wireless/ath/ath11k/dp_tx.c
+++ b/drivers/net/wireless/ath/ath11k/dp_tx.c
@@ -685,7 +685,8 @@ static inline void ath11k_dp_tx_status_parse(struct ath11k_base *ab,
 		ts->rate_stats = 0;
 }
 
-void ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id)
+int ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id,
+				    int budget)
 {
 	struct ath11k *ar;
 	struct ath11k_dp *dp = &ab->dp;
@@ -695,6 +696,7 @@ void ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id)
 	struct hal_tx_status ts = {};
 	struct dp_tx_ring *tx_ring = &dp->tx_ring[ring_id];
 	unsigned long push = 0;
+	int done = 0;
 	u32 *desc;
 	u32 msdu_id;
 	u8 mac_id, i;
@@ -723,10 +725,13 @@ void ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id)
 
 	spin_unlock_bh(&status_ring->lock);
 
-	while (ATH11K_TX_COMPL_NEXT(tx_ring->tx_status_tail) != tx_ring->tx_status_head) {
+	while (done < budget &&
+	       ATH11K_TX_COMPL_NEXT(tx_ring->tx_status_tail) !=
+	       tx_ring->tx_status_head) {
 		struct hal_wbm_release_ring *tx_status;
 		u32 desc_id;
 
+		done++;
 		tx_ring->tx_status_tail =
 			ATH11K_TX_COMPL_NEXT(tx_ring->tx_status_tail);
 		tx_status = &tx_ring->tx_status[tx_ring->tx_status_tail];
@@ -774,6 +779,8 @@ void ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id)
 	for_each_set_bit(i, &push, ab->num_radios * IEEE80211_NUM_ACS)
 		ath11k_mac_tx_push_pending(ab->pdevs[i / IEEE80211_NUM_ACS].ar,
 					   i % IEEE80211_NUM_ACS);
+
+	return done;
 }
 
 int ath11k_dp_tx_send_reo_cmd(struct ath11k_base *ab, struct dp_rx_tid *rx_tid,
diff --git a/drivers/net/wireless/ath/ath11k/dp_tx.h b/drivers/net/wireless/ath/ath11k/dp_tx.h
index 9303b5ba6e01..b5296cda73e3 100644
--- a/drivers/net/wireless/ath/ath11k/dp_tx.h
+++ b/drivers/net/wireless/ath/ath11k/dp_tx.h
@@ -21,7 +21,8 @@ void ath11k_dp_tx_update_txcompl(struct ath11k *ar, struct hal_tx_status *ts);
 int ath11k_dp_tx_htt_h2t_ver_req_msg(struct ath11k_base *ab);
 int ath11k_dp_tx(struct ath11k *ar, struct ath11k_vif *arvif,
 		 struct ath11k_sta *arsta, struct sk_buff *skb);
-void ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id);
+int ath11k_dp_tx_completion_handler(struct ath11k_base *ab, int ring_id,
+				    int budget);
 int ath11k_dp_tx_send_reo_cmd(struct ath11k_base *ab, struct dp_rx_tid *rx_tid,
 			      enum hal_reo_cmd_type type,
 			      struct ath11k_hal_reo_cmd *cmd,
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-09-08 13:53 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-08 13:53 [PATCH ath-next v4 0/9] wifi: ath11k: airtime queue limits, fairness and a driver TXQ scheduler Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 1/9] wifi: ath11k: free tx skbs through ieee80211_free_txskb() Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 2/9] wifi: ath11k: enable airtime queue limits Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 3/9] wifi: ath11k: report the pending tx MSDU count in soc_dp_stats Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 4/9] wifi: ath11k: report tx airtime and enable airtime fairness Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 5/9] wifi: ath11k: schedule TXQs from the driver Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 6/9] wifi: ath11k: run the TXQ scheduler on tx completion Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 7/9] wifi: ath11k: charge received airtime to the station deficit Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 8/9] wifi: ath11k: stop a scheduling round when the hardware refuses a frame Julius Bairaktaris
2026-09-08 13:53 ` [PATCH ath-next v4 9/9] wifi: ath11k: budget the tx completion handler Julius Bairaktaris

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox