* [PATCH net-next 1/2] net/sched: skip empty rounds in the deficit-refill dequeue loops
@ 2026-10-08 15:23 Victor Nogueira
2026-10-08 15:23 ` [PATCH net-next 2/2] selftests: tc-testing: add DRR deficit refill regression tests Victor Nogueira
2026-10-12 15:51 ` [PATCH net-next 1/2] net/sched: skip empty rounds in the deficit-refill dequeue loops netdev-bot+sashiko
0 siblings, 2 replies; 4+ messages in thread
From: Victor Nogueira @ 2026-10-08 15:23 UTC (permalink / raw)
To: davem, edumazet, kuba, pabeni, jhs, jiri, netdev
Cc: horms, sashiko-bot, Shuah Khan
The DRR and ETS dequeue loops replenish one quantum per iteration while
the head packet is larger than the class deficit. A crafted size table
pushes the packet length to QDISC_PKT_LEN_MAX (1 MiB) and the clamped
quantum floor of 256 makes that ~4096 refill iterations per packet, so
a dequeue pass over N active classes costs N * 4096 list walks under
the qdisc spinlock with BH off. A single drain over 16k classes at
quantum 256 takes ~0.9s of BH-off churn, growing with the class count
and stalling traffic on the device.
Skip the rounds during which no active class can dequeue. The first
round runs the existing one-quantum walk unchanged; only after a full
round with no class becoming eligible, compute the fewest whole rounds
no class can dequeue, apply that many quantum refills to every class at
once and resume the walk. A round refills every class once and restores
the list order, so the walk that follows is unchanged: it still
dequeues the first class that becomes eligible in list order, and each
class ends with the same deficit the one-quantum loop converged to. A
class whose child qdisc peeks no packet leaves the original one-quantum
walk (and its non-work-conserving head abort) in sole charge of that
dequeue.
The same refill-and-rotate shape is shared by fq_codel, fq_pie, fq and
sfq, so bound those at the same time. In those flow-based qdiscs a flow
can leave the list mid-walk (emptied, throttled or detached); re-arm
the round sentinel when that happens, and skip a flow that cannot
dequeue on its next visit, so neither case can silently disable the
bound.
This is a follow-up to commit 8f735d64382d ("net/sched: bound
qdisc_pkt_len to prevent qdisc soft lockup"); that capped the packet
length, the refill loop itself has no bound in the number of classes.
It is not a regression and is not marked for stable: the walk has been
linear in the number of classes times the per-class refill count (up to
4096 with a crafted size table) since these schedulers were introduced,
so there is no regression commit to blame and no narrow fix to
backport.
Conditions to recreate:
- CONFIG_NET_SCH_DRR=y, CONFIG_NET_SCH_ETS=y; veth pair
- tc qdisc add dev veth0 root stab mtu 2048 tsize 0 overhead 1048576 drr
- 16384 drr classes quantum 256; one u32 filter flowid 1:1; one ping
per class to arm them
- CAP_NET_ADMIN (namespace-local via unshare -Urn suffices)
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260819143213.57401-1-jhs@mojatatu.com
Reviewed-by: Jamal Hadi Salim <jhs@mojatatu.com>
Signed-off-by: Victor Nogueira <victor@mojatatu.com>
---
net/sched/sch_drr.c | 42 ++++++++++++++++++++++++++-
net/sched/sch_ets.c | 41 ++++++++++++++++++++++++++-
net/sched/sch_fq.c | 61 +++++++++++++++++++++++++++++++++++++++-
net/sched/sch_fq_codel.c | 53 ++++++++++++++++++++++++++++++++--
net/sched/sch_fq_pie.c | 51 +++++++++++++++++++++++++++++++--
net/sched/sch_sfq.c | 42 ++++++++++++++++++++++++++-
6 files changed, 282 insertions(+), 8 deletions(-)
diff --git a/net/sched/sch_drr.c b/net/sched/sch_drr.c
index 8621d057edd9..27d216b777f2 100644
--- a/net/sched/sch_drr.c
+++ b/net/sched/sch_drr.c
@@ -372,7 +372,8 @@ static int drr_enqueue(struct sk_buff *skb, struct Qdisc *sch,
static struct sk_buff *drr_dequeue(struct Qdisc *sch)
{
struct drr_sched *q = qdisc_priv(sch);
- struct drr_class *cl;
+ struct drr_class *cl, *first = NULL;
+ bool scanned = false;
struct sk_buff *skb;
unsigned int len;
@@ -402,6 +403,45 @@ static struct sk_buff *drr_dequeue(struct Qdisc *sch)
return skb;
}
+ /* The head cannot dequeue. Once a full round passes with
+ * no class becoming eligible, skip the remaining empty
+ * rounds in a single bulk refill.
+ */
+ if (!scanned && cl == first) {
+ unsigned int rounds = UINT_MAX;
+ struct drr_class *iter;
+ bool skip = true;
+
+ scanned = true;
+ list_for_each_entry(iter, &q->active, alist) {
+ unsigned int pkt_len;
+ struct sk_buff *peek;
+
+ peek = iter->qdisc->ops->peek(iter->qdisc);
+ if (!peek) {
+ skip = false;
+ break;
+ }
+ pkt_len = qdisc_pkt_len(peek);
+ if (pkt_len <= iter->deficit) {
+ skip = false;
+ break;
+ }
+ rounds = min(rounds,
+ DIV_ROUND_UP(pkt_len - iter->deficit,
+ READ_ONCE(iter->quantum)));
+ }
+ if (skip && rounds != UINT_MAX) {
+ list_for_each_entry(iter, &q->active, alist)
+ WRITE_ONCE(iter->deficit,
+ iter->deficit + rounds *
+ READ_ONCE(iter->quantum));
+ continue;
+ }
+ }
+
+ if (!first)
+ first = cl;
WRITE_ONCE(cl->deficit, cl->deficit + READ_ONCE(cl->quantum));
list_move_tail(&cl->alist, &q->active);
}
diff --git a/net/sched/sch_ets.c b/net/sched/sch_ets.c
index 6cc902a03838..6d96ae2d663b 100644
--- a/net/sched/sch_ets.c
+++ b/net/sched/sch_ets.c
@@ -459,7 +459,8 @@ ets_qdisc_dequeue_skb(struct Qdisc *sch, struct sk_buff *skb)
static struct sk_buff *ets_qdisc_dequeue(struct Qdisc *sch)
{
struct ets_sched *q = qdisc_priv(sch);
- struct ets_class *cl;
+ struct ets_class *cl, *first = NULL;
+ bool scanned = false;
struct sk_buff *skb;
unsigned int band;
unsigned int len;
@@ -493,6 +494,44 @@ static struct sk_buff *ets_qdisc_dequeue(struct Qdisc *sch)
return ets_qdisc_dequeue_skb(sch, skb);
}
+ /* The head cannot dequeue. Once a full round passes with
+ * no band becoming eligible, skip the remaining empty
+ * rounds in a single bulk refill.
+ */
+ if (!scanned && cl == first) {
+ unsigned int rounds = UINT_MAX;
+ struct ets_class *iter;
+ bool skip = true;
+
+ scanned = true;
+ list_for_each_entry(iter, &q->active, alist) {
+ unsigned int pkt_len;
+ struct sk_buff *peek;
+
+ peek = iter->qdisc->ops->peek(iter->qdisc);
+ if (!peek) {
+ skip = false;
+ break;
+ }
+ pkt_len = qdisc_pkt_len(peek);
+ if (pkt_len <= iter->deficit) {
+ skip = false;
+ break;
+ }
+ rounds = min(rounds,
+ DIV_ROUND_UP(pkt_len - iter->deficit,
+ READ_ONCE(iter->quantum)));
+ }
+ if (skip && rounds != UINT_MAX) {
+ list_for_each_entry(iter, &q->active, alist)
+ iter->deficit += rounds *
+ READ_ONCE(iter->quantum);
+ continue;
+ }
+ }
+
+ if (!first)
+ first = cl;
cl->deficit += READ_ONCE(cl->quantum);
list_move_tail(&cl->alist, &q->active);
}
diff --git a/net/sched/sch_fq.c b/net/sched/sch_fq.c
index a282812c192e..125c6bf836b9 100644
--- a/net/sched/sch_fq.c
+++ b/net/sched/sch_fq.c
@@ -737,8 +737,10 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
struct fq_sched_data *q = qdisc_priv(sch);
u64 offload_horizon = fq_offload_horizon(sch, q);
struct fq_perband_flows *pband;
+ struct fq_flow *first = NULL;
struct fq_flow_head *head;
u64 time_next_packet = 0;
+ bool scanned = false;
struct sk_buff *skb;
struct fq_flow *f;
unsigned long rate;
@@ -769,8 +771,16 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
pband = &q->band_flows[q->band_nr];
pband->credit = min(pband->credit + pband->quantum,
pband->quantum);
- if (pband->credit > 0)
+ if (pband->credit > 0) {
+ /* first belongs to the old pband, so
+ * both the sentinel and the scan flag
+ * must be reset here. This is the only
+ * goto begin that changes pband.
+ */
+ first = NULL;
+ scanned = false;
goto begin;
+ }
retry = 0;
}
if (q->time_next_delayed_flow != ~0ULL)
@@ -782,6 +792,45 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
f = head->first;
retry = 0;
if (f->credit <= 0) {
+ /* The head cannot dequeue. Once a full round passes with
+ * no flow becoming eligible, skip the remaining empty
+ * rounds in a single bulk refill.
+ */
+ if (!scanned && f == first) {
+ unsigned int rounds = UINT_MAX;
+ struct fq_flow *iter;
+
+ scanned = true;
+ /* A completed round leaves new_flows empty, so
+ * only old_flows still holds flows awaiting a
+ * refill.
+ */
+ for (iter = pband->old_flows.first; iter;
+ iter = iter->next) {
+ /* A flow that cannot dequeue next round
+ * (empty queue) is removed instead, so it
+ * neither arms nor sizes the skip.
+ */
+ if (!fq_peek(iter))
+ continue;
+ if (iter->credit > 0) {
+ rounds = 0;
+ break;
+ }
+ rounds = min(rounds,
+ DIV_ROUND_UP(1 - iter->credit,
+ q->quantum));
+ }
+ if (rounds && rounds != UINT_MAX) {
+ for (iter = pband->old_flows.first; iter;
+ iter = iter->next)
+ iter->credit += rounds * q->quantum;
+ goto begin;
+ }
+ }
+
+ if (!first)
+ first = f;
f->credit += q->quantum;
head->first = f->next;
fq_flow_add_tail(q, f, OLD_FLOW);
@@ -796,6 +845,11 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
if (now + offload_horizon < time_next_packet) {
head->first = f->next;
f->time_next_packet = time_next_packet;
+ /* first would point at an off-list flow and
+ * the scan would never arm; re-arm it.
+ */
+ if (f == first)
+ first = NULL;
fq_flow_set_throttled(q, f);
goto begin;
}
@@ -814,6 +868,11 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
if (head == &pband->new_flows) {
fq_flow_add_tail(q, f, OLD_FLOW);
} else {
+ /* f leaves the list; re-arm first if it was the
+ * sentinel (see the throttled path above).
+ */
+ if (f == first)
+ first = NULL;
fq_flow_set_detached(f);
}
goto begin;
diff --git a/net/sched/sch_fq_codel.c b/net/sched/sch_fq_codel.c
index e6c87a32950f..79f1f25525e7 100644
--- a/net/sched/sch_fq_codel.c
+++ b/net/sched/sch_fq_codel.c
@@ -284,6 +284,8 @@ static void drop_func(struct sk_buff *skb, void *ctx)
static struct sk_buff *__fq_codel_dequeue(struct Qdisc *sch)
{
struct fq_codel_sched_data *q = qdisc_priv(sch);
+ struct fq_codel_flow *first = NULL;
+ bool scanned = false;
struct sk_buff *skb;
struct fq_codel_flow *flow;
struct list_head *head;
@@ -298,6 +300,47 @@ static struct sk_buff *__fq_codel_dequeue(struct Qdisc *sch)
flow = list_first_entry(head, struct fq_codel_flow, flowchain);
if (flow->deficit <= 0) {
+ /* The head cannot dequeue. Once a full round passes with
+ * no flow becoming eligible, skip the remaining empty
+ * rounds in a single bulk refill.
+ */
+ if (!scanned && flow == first) {
+ unsigned int rounds = UINT_MAX;
+ struct fq_codel_flow *iter;
+ bool skip = true;
+
+ scanned = true;
+ /* A completed round leaves new_flows empty, so
+ * only old_flows still holds flows awaiting a
+ * refill.
+ */
+ list_for_each_entry(iter, &q->old_flows, flowchain) {
+ unsigned int r;
+
+ /* An empty flow cannot dequeue next round
+ * (it is removed instead), so it neither
+ * arms nor sizes the skip.
+ */
+ if (!iter->head)
+ continue;
+ if (iter->deficit > 0) {
+ skip = false;
+ break;
+ }
+ r = DIV_ROUND_UP(1 - iter->deficit, q->quantum);
+ rounds = min(rounds, r);
+ }
+ if (skip && rounds != UINT_MAX) {
+ list_for_each_entry(iter, &q->old_flows, flowchain)
+ WRITE_ONCE(iter->deficit,
+ iter->deficit +
+ rounds * q->quantum);
+ goto begin;
+ }
+ }
+
+ if (!first)
+ first = flow;
WRITE_ONCE(flow->deficit, flow->deficit + q->quantum);
list_move_tail(&flow->flowchain, &q->old_flows);
goto begin;
@@ -309,10 +352,16 @@ static struct sk_buff *__fq_codel_dequeue(struct Qdisc *sch)
if (!skb) {
/* force a pass through old_flows to prevent starvation */
- if ((head == &q->new_flows) && !list_empty(&q->old_flows))
+ if (head == &q->new_flows && !list_empty(&q->old_flows)) {
list_move_tail(&flow->flowchain, &q->old_flows);
- else
+ } else {
+ /* first would point at an off-list flow and the
+ * scan would never arm; re-arm it.
+ */
+ if (flow == first)
+ first = NULL;
list_del_init(&flow->flowchain);
+ }
goto begin;
}
qdisc_bstats_update(sch, skb);
diff --git a/net/sched/sch_fq_pie.c b/net/sched/sch_fq_pie.c
index 5982847df8f8..2cd54516a78e 100644
--- a/net/sched/sch_fq_pie.c
+++ b/net/sched/sch_fq_pie.c
@@ -238,6 +238,8 @@ static inline struct sk_buff *dequeue_head(struct fq_pie_flow *flow)
static struct sk_buff *fq_pie_qdisc_dequeue(struct Qdisc *sch)
{
struct fq_pie_sched_data *q = qdisc_priv(sch);
+ struct fq_pie_flow *first = NULL;
+ bool scanned = false;
struct sk_buff *skb = NULL;
struct fq_pie_flow *flow;
struct list_head *head;
@@ -254,6 +256,45 @@ static struct sk_buff *fq_pie_qdisc_dequeue(struct Qdisc *sch)
flow = list_first_entry(head, struct fq_pie_flow, flowchain);
/* Flow has exhausted all its credits */
if (flow->deficit <= 0) {
+ /* The head cannot dequeue. Once a full round passes with
+ * no flow becoming eligible, skip the remaining empty
+ * rounds in a single bulk refill.
+ */
+ if (!scanned && flow == first) {
+ unsigned int rounds = UINT_MAX;
+ struct fq_pie_flow *iter;
+ bool skip = true;
+
+ scanned = true;
+ /* A completed round leaves new_flows empty, so
+ * only old_flows still holds flows awaiting a
+ * refill.
+ */
+ list_for_each_entry(iter, &q->old_flows, flowchain) {
+ unsigned int r;
+
+ /* An empty flow cannot dequeue next round
+ * (it is removed instead), so it neither
+ * arms nor sizes the skip.
+ */
+ if (!iter->head)
+ continue;
+ if (iter->deficit > 0) {
+ skip = false;
+ break;
+ }
+ r = DIV_ROUND_UP(1 - iter->deficit, q->quantum);
+ rounds = min(rounds, r);
+ }
+ if (skip && rounds != UINT_MAX) {
+ list_for_each_entry(iter, &q->old_flows, flowchain)
+ iter->deficit += rounds * q->quantum;
+ goto begin;
+ }
+ }
+
+ if (!first)
+ first = flow;
flow->deficit += q->quantum;
list_move_tail(&flow->flowchain, &q->old_flows);
goto begin;
@@ -269,10 +310,16 @@ static struct sk_buff *fq_pie_qdisc_dequeue(struct Qdisc *sch)
if (!skb) {
/* force a pass through old_flows to prevent starvation */
- if (head == &q->new_flows && !list_empty(&q->old_flows))
+ if (head == &q->new_flows && !list_empty(&q->old_flows)) {
list_move_tail(&flow->flowchain, &q->old_flows);
- else
+ } else {
+ /* first would point at an off-list flow and the
+ * scan would never arm; re-arm it.
+ */
+ if (flow == first)
+ first = NULL;
list_del_init(&flow->flowchain);
+ }
goto begin;
}
diff --git a/net/sched/sch_sfq.c b/net/sched/sch_sfq.c
index 8bbcfc9e85d9..e160de8af5e1 100644
--- a/net/sched/sch_sfq.c
+++ b/net/sched/sch_sfq.c
@@ -479,9 +479,11 @@ static struct sk_buff *
sfq_dequeue(struct Qdisc *sch)
{
struct sfq_sched_data *q = qdisc_priv(sch);
+ sfq_index first = SFQ_EMPTY_SLOT;
struct sk_buff *skb;
- sfq_index a, next_a;
struct sfq_slot *slot;
+ bool scanned = false;
+ sfq_index a, next_a;
/* No active slots */
if (q->tail == NULL)
@@ -491,6 +493,44 @@ sfq_dequeue(struct Qdisc *sch)
a = q->tail->next;
slot = &q->slots[a];
if (slot->allot <= 0) {
+ /* The head cannot dequeue. Once a full round passes with
+ * no slot becoming eligible, skip the remaining empty
+ * rounds in a single bulk refill.
+ */
+ if (!scanned && a == first) {
+ unsigned int rounds = UINT_MAX;
+ bool skip = true;
+ sfq_index i = a;
+
+ scanned = true;
+ do {
+ struct sfq_slot *s = &q->slots[i];
+ unsigned int r;
+
+ if (s->allot > 0) {
+ skip = false;
+ break;
+ }
+ r = DIV_ROUND_UP(1 - s->allot, q->quantum);
+ rounds = min(rounds, r);
+ i = s->next;
+ } while (i != a);
+ if (skip && rounds != UINT_MAX) {
+ i = a;
+ do {
+ struct sfq_slot *s = &q->slots[i];
+
+ WRITE_ONCE(s->allot,
+ s->allot +
+ rounds * q->quantum);
+ i = s->next;
+ } while (i != a);
+ goto next_slot;
+ }
+ }
+
+ if (!scanned && first == SFQ_EMPTY_SLOT)
+ first = a;
q->tail = slot;
WRITE_ONCE(slot->allot, slot->allot + q->quantum);
goto next_slot;
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH net-next 2/2] selftests: tc-testing: add DRR deficit refill regression tests
2026-10-08 15:23 [PATCH net-next 1/2] net/sched: skip empty rounds in the deficit-refill dequeue loops Victor Nogueira
@ 2026-10-08 15:23 ` Victor Nogueira
2026-10-12 15:51 ` netdev-bot+sashiko
2026-10-12 15:51 ` [PATCH net-next 1/2] net/sched: skip empty rounds in the deficit-refill dequeue loops netdev-bot+sashiko
1 sibling, 1 reply; 4+ messages in thread
From: Victor Nogueira @ 2026-10-08 15:23 UTC (permalink / raw)
To: davem, edumazet, kuba, pabeni, jhs, jiri, netdev
Cc: horms, sashiko-bot, Shuah Khan, linux-kselftest
Add two DRR cases.
The first asserts the post-dequeue deficit of a class with quantum 256
after one 1514-byte packet is serviced: 256 +
DIV_ROUND_UP(1514 - 256, 256) * 256 - 1514 = 22 bytes, read via
'tc -s class show' while a second packet is still queued behind a
netem-delayed child qdisc.
The second guards the service order of a multi-class round: with two
active quantum-256 classes, the list head holding a 1514-byte packet
and the next class a 514-byte one, the small packet becomes eligible
first and is the one dequeued. A per-class bulk refill that advances
each class to its own eligibility point would dequeue the head class
instead.
The two cases verify differently on purpose: the deficit case reads an
xstats field that tc prints only through its FP format string (it is
not present in JSON output at all), so it must use matchPattern; the
order case asserts the class stats and uses matchJSON for the richer
backlog/qlen coverage.
Both cases use a netem child under DRR, which peeks NULL until its
delay expires, so both emit a rate-limited
"drr_dequeue: qdisc ... is non-work-conserving?" line into dmesg.
That is pre-existing behaviour for any netem-under-classful test
(the infra 1f1f case does the same) and is expected, not a failure.
Both cases assert the loop-equivalence invariant (deficit convergence
and service order) and pass on the unpatched kernel by construction;
they pin it against a future rewrite.
Signed-off-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
.../tc-testing/tc-tests/qdiscs/drr.json | 82 +++++++++++++++++++
1 file changed, 82 insertions(+)
diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/drr.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/drr.json
index 2b61d8d79bde..bb0995d8358f 100644
--- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/drr.json
+++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/drr.json
@@ -86,5 +86,87 @@
"teardown": [
"$TC qdisc del dev $DUMMY root"
]
+ },
+ {
+ "id": "3f3e",
+ "name": "DRR deficit refill converges to the loop-equivalent value",
+ "category": [
+ "qdisc",
+ "drr"
+ ],
+ "plugins": {
+ "requires": "nsPlugin"
+ },
+ "setup": [
+ "$IP addr add 10.10.11.10/24 dev $DUMMY",
+ "$TC qdisc add dev $DUMMY root handle 1: drr",
+ "$TC class add dev $DUMMY parent 1: classid 1:1 drr quantum 256",
+ "$TC qdisc add dev $DUMMY parent 1:1 handle 10: netem delay 2s limit 10",
+ "$TC filter add dev $DUMMY parent 1: protocol ip prio 1 u32 match u32 0 0 flowid 1:1",
+ "ping -c 2 -s 1472 -i 1 10.10.11.11 -I $DUMMY -W 0.01 >/dev/null 2>&1 &"
+ ],
+ "cmdUnderTest": "sleep 2.5",
+ "expExitCode": "0",
+ "verifyCmd": "$TC -s class show dev $DUMMY",
+ "matchPattern": "deficit 22b",
+ "matchCount": "1",
+ "teardown": [
+ "$TC qdisc del dev $DUMMY root"
+ ]
+ },
+ {
+ "id": "57a1",
+ "name": "DRR skips only rounds where no class can dequeue (keeps order)",
+ "category": [
+ "qdisc",
+ "drr"
+ ],
+ "plugins": {
+ "requires": "nsPlugin"
+ },
+ "setup": [
+ "$IP addr add 10.10.11.10/24 dev $DUMMY",
+ "$TC qdisc add dev $DUMMY root handle 1: drr",
+ "$TC class add dev $DUMMY parent 1: classid 1:1 drr quantum 256",
+ "$TC class add dev $DUMMY parent 1: classid 1:2 drr quantum 256",
+ "$TC qdisc add dev $DUMMY parent 1:1 handle 10: netem delay 2s limit 10",
+ "$TC qdisc add dev $DUMMY parent 1:2 handle 20: netem delay 2s limit 10",
+ "$TC filter add dev $DUMMY parent 1: protocol ip prio 1 u32 match ip dport 1111 0xffff flowid 1:1",
+ "$TC filter add dev $DUMMY parent 1: protocol ip prio 1 u32 match ip dport 2222 0xffff flowid 1:2",
+ "python3 -c \"import socket; a=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); b=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); a.sendto(b'A'*1472,('10.10.11.11',1111)); b.sendto(b'B'*472,('10.10.11.11',2222))\" >/dev/null 2>&1 &",
+ "sleep 1",
+ "python3 -c \"import socket; a=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); b=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); a.sendto(b'A'*1472,('10.10.11.11',1111)); b.sendto(b'B'*472,('10.10.11.11',2222))\" >/dev/null 2>&1 &"
+ ],
+ "cmdUnderTest": "sleep 1.5",
+ "expExitCode": "0",
+ "verifyCmd": "$TC -j -s class show dev $DUMMY",
+ "matchJSON": [
+ {
+ "class": "drr",
+ "handle": "1:1",
+ "root": true,
+ "stats": {
+ "bytes": 0,
+ "packets": 0,
+ "backlog": 3028,
+ "qlen": 2
+ }
+ },
+ {
+ "class": "drr",
+ "handle": "1:2",
+ "root": true,
+ "stats": {
+ "bytes": 514,
+ "packets": 1,
+ "backlog": 514,
+ "qlen": 1
+ }
+ }
+ ],
+ "matchCount": "1",
+ "teardown": [
+ "$TC qdisc del dev $DUMMY root"
+ ]
}
]
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net-next 1/2] net/sched: skip empty rounds in the deficit-refill dequeue loops
2026-10-08 15:23 [PATCH net-next 1/2] net/sched: skip empty rounds in the deficit-refill dequeue loops Victor Nogueira
2026-10-08 15:23 ` [PATCH net-next 2/2] selftests: tc-testing: add DRR deficit refill regression tests Victor Nogueira
@ 2026-10-12 15:51 ` netdev-bot+sashiko
1 sibling, 0 replies; 4+ messages in thread
From: netdev-bot+sashiko @ 2026-10-12 15:51 UTC (permalink / raw)
To: victor
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, netdev, horms,
sashiko-bot, shuah
Thank you for your contribution! Sashiko AI review found 8 potential
issue(s) to consider.
Critical: 0 · High: 4 · Medium: 2 · Low: 2
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] In fq_dequeue() and __fq_codel_dequeue(), the bulk skip runs at
most once per dequeue call.
- [High] The commit message lists the qdiscs that share the 'refill-and-
rotate shape' (fq_codel, fq_pie, fq, sfq) and bounds them, but…
- [High] In __fq_codel_dequeue() and fq_pie_qdisc_dequeue(), and per band
in fq_dequeue(), the bulk refill never fires when every flow waiting…
- [High] In fq_dequeue(), the round sentinel `first` assumes that a flow
refilled through `fq_flow_add_tail(q, f, OLD_FLOW)` stays on the…
- [Medium] In fq_dequeue(), __fq_codel_dequeue() and
fq_pie_qdisc_dequeue(), the scan that sizes `rounds` skips empty flows…
- [Medium] In fq_dequeue(), __fq_codel_dequeue() and
fq_pie_qdisc_dequeue(), the sentinel is set with `if (!first) first =…
- [Low] The patch fixes a BH-off stall under the qdisc lock that, per its
own reproducer, can be triggered from an unprivileged user namespace…
- [Low] drr_dequeue() and ets_qdisc_dequeue() now read each class's
quantum twice.
--
net/sched: skip empty rounds in the deficit-refill dequeue loops
This patch bounds the deficit-refill dequeue loops in drr, ets, fq,
fq_codel, fq_pie and sfq. After one full round in which no class or flow
becomes eligible, it computes the minimum number of rounds needed and
applies that many quantum refills to every class or flow at once.
Closes: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260819143213.57401-1-jhs@mojatatu.com
> The same refill-and-rotate shape is shared by fq_codel, fq_pie, fq and
> sfq, so bound those at the same time.
[Severity: High]
Should sch_cake be on this list too? cake_dequeue() has the same
refill-and-rotate loop, and this patch leaves it unchanged:
net/sched/sch_cake.c:cake_dequeue() {
...
WRITE_ONCE(flow->deficit,
flow->deficit + cake_get_flow_quantum(b, flow, q->config->flow_mode));
list_move_tail(&flow->flowchain, &b->old_flows);
goto retry;
...
}
The deficit is charged with the length from cake_overhead(), which starts
from qdisc_pkt_len(). The same crafted stab can therefore push it to
QDISC_PKT_LEN_MAX.
The quantum added per refill also shrinks as host_load grows:
net/sched/sch_cake.c:cake_get_flow_quantum() {
...
return (q->flow_quantum * quantum_div[host_load] +
get_random_u16()) >> 16;
}
flow_quantum is clamped to 300..1514 and quantum_div[i] is 65535 / i. In
the default triple-isolate mode, host_load can reach CAKE_QUEUES (1024), so
the average refill can be well under one byte.
Could a 1 MiB charge then take millions of refills per flow? That would
apply to up to 1024 flows per tin, all under the qdisc lock with BH off.
> It is not a regression and is not marked for stable: the walk has been
> linear in the number of classes times the per-class refill count (up to
> 4096 with a crafted size table) since these schedulers were introduced,
> so there is no regression commit to blame and no narrow fix to
> backport.
[Severity: Low]
Does this match how the predecessor was tagged? Commit 8f735d64382d
("net/sched: bound qdisc_pkt_len to prevent qdisc soft lockup") covers the
same bug class with the same unshare -Urn reproduction path, and it
carries:
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Without a Fixes: tag here, stable kernels get the 1 MiB cap but keep the
N * 4096 multi-class stall. The reproducer above shows that stall can be
triggered with namespace-local CAP_NET_ADMIN.
> diff --git a/net/sched/sch_drr.c b/net/sched/sch_drr.c
> index 8621d057edd9f..27d216b777f24 100644
> --- a/net/sched/sch_drr.c
> +++ b/net/sched/sch_drr.c
[ ... ]
> @@ -402,6 +403,45 @@ static struct sk_buff *drr_dequeue(struct Qdisc *sch)
> return skb;
> }
>
> + /* The head cannot dequeue. Once a full round passes with
> + * no class becoming eligible, skip the remaining empty
> + * rounds in a single bulk refill.
> + */
> + if (!scanned && cl == first) {
> + unsigned int rounds = UINT_MAX;
> + struct drr_class *iter;
> + bool skip = true;
> +
> + scanned = true;
> + list_for_each_entry(iter, &q->active, alist) {
[ ... ]
> + rounds = min(rounds,
> + DIV_ROUND_UP(pkt_len - iter->deficit,
> + READ_ONCE(iter->quantum)));
> + }
> + if (skip && rounds != UINT_MAX) {
> + list_for_each_entry(iter, &q->active, alist)
> + WRITE_ONCE(iter->deficit,
> + iter->deficit + rounds *
> + READ_ONCE(iter->quantum));
[Severity: Low]
Can a concurrent quantum change over-credit a class here? quantum is read
once to size rounds and again to apply it.
drr_change_class() updates an existing class while holding only RTNL, not
the root qdisc lock:
if (tb[TCA_DRR_QUANTUM])
WRITE_ONCE(cl->quantum, quantum);
Suppose the quantum goes from 256 to 1 << 20 between the two reads while
rounds is about 4094. The class then gets roughly 4 GiB of deficit at
once, while the one-quantum walk could give at most pkt_len plus the new
quantum. That class could then dequeue a long run of packets while the
other active classes starve.
ets_qdisc_dequeue() has the same double read, with ets_class_change() as
the writer.
> + continue;
> + }
> + }
[ ... ]
> diff --git a/net/sched/sch_fq.c b/net/sched/sch_fq.c
> index a282812c192ee..125c6bf836b9f 100644
> --- a/net/sched/sch_fq.c
> +++ b/net/sched/sch_fq.c
[ ... ]
> @@ -782,6 +792,45 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
> f = head->first;
> retry = 0;
> if (f->credit <= 0) {
> + /* The head cannot dequeue. Once a full round passes with
> + * no flow becoming eligible, skip the remaining empty
> + * rounds in a single bulk refill.
> + */
> + if (!scanned && f == first) {
> + unsigned int rounds = UINT_MAX;
> + struct fq_flow *iter;
> +
> + scanned = true;
> + /* A completed round leaves new_flows empty, so
> + * only old_flows still holds flows awaiting a
> + * refill.
> + */
> + for (iter = pband->old_flows.first; iter;
> + iter = iter->next) {
> + /* A flow that cannot dequeue next round
> + * (empty queue) is removed instead, so it
> + * neither arms nor sizes the skip.
> + */
> + if (!fq_peek(iter))
> + continue;
> + if (iter->credit > 0) {
> + rounds = 0;
> + break;
> + }
> + rounds = min(rounds,
> + DIV_ROUND_UP(1 - iter->credit,
> + q->quantum));
> + }
> + if (rounds && rounds != UINT_MAX) {
> + for (iter = pband->old_flows.first; iter;
> + iter = iter->next)
> + iter->credit += rounds * q->quantum;
[Severity: Medium]
Does this refill over-credit empty flows? The scan skips flows with no
packet, but the refill loop adds rounds * q->quantum to every flow on
old_flows, including the empty ones.
In the one-quantum walk, an empty flow with credit <= 0 is refilled only
until it turns positive. It is then detached with credit in (0, quantum].
An empty flow that already had credit > 0 is never refilled at all.
Here, an empty flow that needed fewer rounds than rounds (or none) can be
left with up to about (rounds - r) * quantum of extra credit, where r is
the number of rounds that flow needed itself.
fq_enqueue() keeps that credit across detach and re-attach:
if (fq_flow_is_detached(f)) {
fq_flow_add_tail(q, f, NEW_FLOW);
if (time_after(jiffies, f->age + q->flow_refill_delay))
f->credit = max_t(u32, f->credit, q->quantum);
}
so the flow can burst above its fair share in its next active period.
__fq_codel_dequeue() and fq_pie_qdisc_dequeue() have the same mismatch. In
those, the deficit is reset only when the flow was unlinked:
if (list_empty(&flow->flowchain)) {
list_add_tail(&flow->flowchain, &q->new_flows);
q->new_flow_count++;
WRITE_ONCE(flow->deficit, q->quantum);
}
Suppose the flow that dequeues comes before the empty flow in list order.
The empty flow is then still linked when its next packet arrives. It keeps
the inflated deficit and is not promoted to new_flows.
This seems to contradict the comment that empty flows "neither arm nor
size the skip". It also seems to contradict the commit message's claim
that "each class ends with the same deficit the one-quantum loop converged
to". Is that intended?
> + goto begin;
> + }
> + }
> +
> + if (!first)
> + first = f;
> f->credit += q->quantum;
> head->first = f->next;
> fq_flow_add_tail(q, f, OLD_FLOW);
[Severity: High]
Can the sentinel leave this band's list here? fq_flow_add_tail() picks
the destination list from flow->band:
struct fq_perband_flows *pband = &q->band_flows[flow->band];
fq_enqueue() sets the band on every enqueue, even when the flow is already
linked on another band's list:
f->band = band;
q->band_pkt_count[band]++;
fq_skb_cb(skb)->band = band;
Suppose the sentinel flow got a later packet whose priority maps to
another band, for example after a SO_PRIORITY change on the same socket.
This refill then moves it onto that band's old_flows. first is not
cleared, so f == first never matches again during this pband walk. The
scan never runs, and the walk falls back to one quantum per iteration.
The throttle and detach exits re-arm first, but this path does not.
Should it?
> @@ -796,6 +845,11 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
> if (now + offload_horizon < time_next_packet) {
> head->first = f->next;
> f->time_next_packet = time_next_packet;
> + /* first would point at an off-list flow and
> + * the scan would never arm; re-arm it.
> + */
> + if (f == first)
> + first = NULL;
> fq_flow_set_throttled(q, f);
> goto begin;
> }
[Severity: High]
Does re-arming first help once the scan has already run? The scan is
gated on "!scanned && f == first", and scanned is reset only on a band
switch. After one scan, clearing first here only picks a new sentinel
that can never trigger another scan.
The scan treats every flow with fq_peek() != NULL as able to dequeue. It
does not look at time_to_send or time_next_packet.
Take a paced or SO_TXTIME flow that becomes eligible, either through the
bulk refill or by turning positive in the first round (which makes the
scan abort with rounds = 0). It then reaches the head with credit > 0 and
is throttled here without dequeuing.
The remaining flows go back to one quantum per iteration for all their
remaining rounds. That is the N * 4096 walk under the qdisc lock with BH
off.
__fq_codel_dequeue() has the same problem. codel_dequeue() can drop every
remaining packet of the eligible flow, because codel_should_drop() looks
at the whole qdisc backlog. The flow is then list_del_init()'d while
scanned is already true.
Is the commit message's claim that "neither case can silently disable the
bound" still accurate once the scan has been used?
[ ... ]
> diff --git a/net/sched/sch_fq_codel.c b/net/sched/sch_fq_codel.c
> index e6c87a32950f9..79f1f25525e74 100644
> --- a/net/sched/sch_fq_codel.c
> +++ b/net/sched/sch_fq_codel.c
[ ... ]
> @@ -298,6 +300,47 @@ static struct sk_buff *__fq_codel_dequeue(struct Qdisc *sch)
> flow = list_first_entry(head, struct fq_codel_flow, flowchain);
>
> if (flow->deficit <= 0) {
[ ... ]
> + list_for_each_entry(iter, &q->old_flows, flowchain) {
> + unsigned int r;
> +
> + /* An empty flow cannot dequeue next round
> + * (it is removed instead), so it neither
> + * arms nor sizes the skip.
> + */
> + if (!iter->head)
> + continue;
> + if (iter->deficit > 0) {
> + skip = false;
> + break;
> + }
> + r = DIV_ROUND_UP(1 - iter->deficit, q->quantum);
> + rounds = min(rounds, r);
> + }
> + if (skip && rounds != UINT_MAX) {
[Severity: High]
What happens when every flow on old_flows is empty? rounds stays UINT_MAX
and the code falls through to the one-quantum walk.
The comment also does not match how empty flows are handled. An empty
flow with deficit <= 0 is refilled and rotated. It is removed only once
its deficit turns positive.
For example, N flows each send one packet with a 1 MiB stab-inflated
pkt_len, at quantum 256. Each flow is drained and left on old_flows with a
deficit of about quantum - 1 MiB.
Neither fq_codel_dequeue() nor fq_pie_qdisc_dequeue() checks qlen, and
dequeue_skb() calls q->dequeue() without checking it either. The next
dequeue call then does about 4096 refills per flow before each flow is
list_del_init()'d, and it returns no packet.
Isn't that the same N * 4096 walk under the qdisc lock that this patch is
meant to bound?
fq_pie_qdisc_dequeue() has the same structure. fq_dequeue() checks
sch->q.qlen, but the same per-band walk can happen in two cases: the
serviced band's old_flows are all empty with negative credit, and another
band still holds packets.
> + list_for_each_entry(iter, &q->old_flows, flowchain)
> + WRITE_ONCE(iter->deficit,
> + iter->deficit +
> + rounds * q->quantum);
> + goto begin;
> + }
> + }
> +
> + if (!first)
> + first = flow;
> WRITE_ONCE(flow->deficit, flow->deficit + q->quantum);
> list_move_tail(&flow->flowchain, &q->old_flows);
> goto begin;
[Severity: Medium]
Can a run of empty flows stop the scan from ever running? The sentinel is
chosen without checking whether the flow holds a packet.
Consider old_flows ordered as empty flows E1..Ek, where Ei has deficit
1 - i * Q, followed by M backlogged flows with deficit 1 - (k + 1) * Q:
E1 becomes first and turns positive after one refill.
On its next visit, E1 takes the list_del_init() path, which clears
first.
E2 becomes the sentinel and turns positive at its next refill, and so
on.
No sentinel is ever revisited with a non-positive deficit, so the scan
never runs. Each backlogged flow takes about k single refills. That is
O(k * (M + k)) visits under the qdisc lock.
The same pattern applies to fq_pie_qdisc_dequeue(), and to fq_dequeue()
through the fq_flow_set_detached() path. I have not confirmed that
userspace can reliably build this staggered-deficit ordering, though.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-TIAE.v1.20261008110502%40mojatatu.com
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net-next 2/2] selftests: tc-testing: add DRR deficit refill regression tests
2026-10-08 15:23 ` [PATCH net-next 2/2] selftests: tc-testing: add DRR deficit refill regression tests Victor Nogueira
@ 2026-10-12 15:51 ` netdev-bot+sashiko
0 siblings, 0 replies; 4+ messages in thread
From: netdev-bot+sashiko @ 2026-10-12 15:51 UTC (permalink / raw)
To: victor
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, netdev, horms,
sashiko-bot, shuah
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 2 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] Both new tdc cases in tools/testing/selftests/tc-testing/tc-
tests/qdiscs/drr.json only pass if the verify command lands inside a…
- [Medium] Test 57a1 is meant to catch a 'per-class bulk refill that
advances each class to its own eligibility point', as the commit…
--
selftests: tc-testing: add DRR deficit refill regression tests
This adds two tdc cases to drr.json. The first checks the deficit left on a
quantum 256 class after one 1514 byte packet is served (22 bytes), while a
second packet is still queued behind a netem child. The second checks
service order: with two active quantum 256 classes, the 514 byte packet
should be dequeued before the 1514 byte packet at the list head.
> diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/drr.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/drr.json
> index 2b61d8d79bde8..bb0995d8358fc 100644
> --- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/drr.json
> +++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/drr.json
> @@ -86,5 +86,87 @@
[ ... ]
> + {
> + "id": "3f3e",
> + "name": "DRR deficit refill converges to the loop-equivalent value",
[ ... ]
> + "setup": [
> + "$IP addr add 10.10.11.10/24 dev $DUMMY",
> + "$TC qdisc add dev $DUMMY root handle 1: drr",
> + "$TC class add dev $DUMMY parent 1: classid 1:1 drr quantum 256",
> + "$TC qdisc add dev $DUMMY parent 1:1 handle 10: netem delay 2s limit 10",
> + "$TC filter add dev $DUMMY parent 1: protocol ip prio 1 u32 match u32 0 0 flowid 1:1",
> + "ping -c 2 -s 1472 -i 1 10.10.11.11 -I $DUMMY -W 0.01 >/dev/null 2>&1 &"
> + ],
> + "cmdUnderTest": "sleep 2.5",
> + "expExitCode": "0",
> + "verifyCmd": "$TC -s class show dev $DUMMY",
> + "matchPattern": "deficit 22b",
[Severity: Medium]
Can this case fail on a kernel that is behaving correctly, purely because of
timing?
The pattern only matches inside a window about one second wide. Say ping
sends packet 1 at T0 and packet 2 at T0+1. With netem delay 2s, they are
released at T0+2 and T0+3:
before T0+2: deficit is still 256, tc prints "deficit 256b"
T0+2 .. T0+3: packet 1 served, deficit is 22
after T0+3: packet 2 dequeued, child qlen drops to 0
Once qlen is 0, the dump reports zero:
net/sched/sch_drr.c:drr_dump_class_stats() {
...
memset(&xstats, 0, sizeof(xstats));
if (qlen)
xstats.deficit = READ_ONCE(cl->deficit);
...
}
so tc prints "deficit 0b".
The verify command runs at the end of setup, plus the sleep 2.5, plus the
cost of spawning cmdUnderTest and verifyCmd through ip netns exec. That
leaves about 0.5s of slack either way for the difference between that
overhead and ping's startup time.
Test 57a1 below has the same shape. B1 is released about two seconds after
the first python3 sends. B2 is released about two seconds after the second
python3 sends. At that point 1:2 is at the head with a deficit of 254 and a
514 byte packet. The refill then serves both B2 and A1, which changes every
bytes/packets/backlog/qlen value in its matchJSON.
The verify at setup end plus sleep 1.5 has to land between those two
releases. That again depends on python3 startup and spawn overhead staying
under about 0.5s.
tdc.sh runs ./tdc.py -J"$(nproc)", so these cases run in parallel with
other tests. That adds fork/exec, python3 startup and scheduling jitter,
especially on debug kernels with KASAN or lockdep.
Most existing sleep based tdc cases only need a lower bound. These two need
both a lower and an upper bound.
> + "matchCount": "1",
> + "teardown": [
> + "$TC qdisc del dev $DUMMY root"
> + ]
> + },
> + {
> + "id": "57a1",
> + "name": "DRR skips only rounds where no class can dequeue (keeps order)",
[ ... ]
> + "setup": [
> + "$IP addr add 10.10.11.10/24 dev $DUMMY",
> + "$TC qdisc add dev $DUMMY root handle 1: drr",
> + "$TC class add dev $DUMMY parent 1: classid 1:1 drr quantum 256",
> + "$TC class add dev $DUMMY parent 1: classid 1:2 drr quantum 256",
> + "$TC qdisc add dev $DUMMY parent 1:1 handle 10: netem delay 2s limit 10",
> + "$TC qdisc add dev $DUMMY parent 1:2 handle 20: netem delay 2s limit 10",
[Severity: Medium]
Does this case reliably catch the regression the commit message describes?
A per-class bulk refill that advances each class to its own
eligibility point would dequeue the head class instead.
To tell the two refill styles apart, A1 (in 1:1, the list head) and B1 (in
1:2) both have to be ready in the same drr_dequeue() pass.
Both netem children use delay 2s. netem_dequeue() compares time_to_send
against ktime_get_ns() at peek time. So B1 becomes sendable a small delta
after A1, where the delta is the gap between the two sendto() calls in the
same python3 process.
What happens if the hrtimer -> NET_TX softirq -> drr_dequeue() latency is
shorter than that delta?
1. At A1's release, 1:1 peeks A1 and 1514 > 256. first is set to 1:1, its
deficit becomes 512, and list_move_tail() leaves the list as [1:2, 1:1].
2. 1:2 peeks NULL, and drr_dequeue() bails out:
if (skb == NULL) {
qdisc_warn_nonwc(__func__, cl->qdisc);
goto out;
}
1:2 is now the list head.
3. At B1's release, 1:2 goes 256 -> 512 and 1:1 goes 512 -> 768. The bulk
refill scan, with 1:2 at the head, computes
rounds = min(DIV_ROUND_UP(2, 256), DIV_ROUND_UP(746, 256)) = 1. That
gives 1:2 = 768 and 1:1 = 1024, and B1 is dequeued.
In that state, a per-class variant would give 1:2 = 768 and 1:1 = 1536. It
would still visit 1:2 first and dequeue B1, so the matchJSON below would
still match.
The test only fails as intended when B1 is already sendable at A1's
release. Then the scan starts with 1:1 at the head and both classes at 512.
The ratio between that delta and softirq latency varies across bare metal,
KVM and TCG. Could netem 20 use a slightly shorter delay than netem 10? That
way B1 is always ready when child 10's watchdog fires.
> + "$TC filter add dev $DUMMY parent 1: protocol ip prio 1 u32 match ip dport 1111 0xffff flowid 1:1",
> + "$TC filter add dev $DUMMY parent 1: protocol ip prio 1 u32 match ip dport 2222 0xffff flowid 1:2",
[ ... ]
> + "matchJSON": [
> + {
> + "class": "drr",
> + "handle": "1:1",
> + "root": true,
> + "stats": {
> + "bytes": 0,
> + "packets": 0,
> + "backlog": 3028,
> + "qlen": 2
> + }
> + },
> + {
> + "class": "drr",
> + "handle": "1:2",
> + "root": true,
> + "stats": {
> + "bytes": 514,
> + "packets": 1,
> + "backlog": 514,
> + "qlen": 1
> + }
> + }
> + ],
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-TIAE.v1.20261008110502%40mojatatu.com
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-12 15:51 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-08 15:23 [PATCH net-next 1/2] net/sched: skip empty rounds in the deficit-refill dequeue loops Victor Nogueira
2026-10-08 15:23 ` [PATCH net-next 2/2] selftests: tc-testing: add DRR deficit refill regression tests Victor Nogueira
2026-10-12 15:51 ` netdev-bot+sashiko
2026-10-12 15:51 ` [PATCH net-next 1/2] net/sched: skip empty rounds in the deficit-refill dequeue loops netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox