* [PATCH net-next v6 0/6] hardware pacing offload
@ 2026-08-13 2:03 Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 1/6] net: rtnetlink: add pacing_offload_horizon attribute to net_device Willem de Bruijn
` (5 more replies)
0 siblings, 6 replies; 8+ messages in thread
From: Willem de Bruijn @ 2026-08-13 2:03 UTC (permalink / raw)
To: netdev
Cc: davem, kuba, edumazet, pabeni, horms, andrew+netdev,
Willem de Bruijn
From: Willem de Bruijn <willemb@google.com>
Enable safe hardware pacing offload on a first device (idpf), plus support.
- Add rtnetlink support for admin control over the device feature
- Add device support to the idpf driver
Besides the main feature, also
- Small optimization to FQ offload: avoid unnecessary ktime_.._get().
- Expand so_txtime drv-net test with hw offload testcases.
- Expand pktgen with txtime feature.
Details and detailed changelog in the individual patches.
High level changes:
v5 -> v6
- Address feedback
v4 -> v5
- Rebase + small kdoc fix only
v3 -> v4
- Replace ethtool API with rtnetlink
- Drop/defer pktgen patch: not essential
NOTE: the idpf patch will have a minor merge conflict with commit
"idpf: add missing cpu_to_le32 in idpf_tx_splitq_build_flow_desc".
The end-result should look as in this series.
(https://lore.kernel.org/netdev/20260803210707.1912217-1-willemdebruijn.kernel@gmail.com/).
v5: https://lore.kernel.org/netdev/20260808155217.885299-1-willemdebruijn.kernel@gmail.com/
v4: https://lore.kernel.org/netdev/20260806232913.4020403-1-willemdebruijn.kernel@gmail.com/
v3: https://lore.kernel.org/netdev/20260726202902.760552-1-willemdebruijn.kernel@gmail.com/
v2: https://lore.kernel.org/netdev/20260722204454.3234605-1-willemdebruijn.kernel@gmail.com/
v1: https://lore.kernel.org/netdev/20260706133433.3142805-1-willemdebruijn.kernel@gmail.com/
Willem de Bruijn (6):
net: rtnetlink: add pacing_offload_horizon attribute to net_device
net_sched: sch_fq: clear past skb->tstamp if offloading pacing
idpf: support pacing offload
selftests: drv-net: refactor so_txtime errqueue handling
selftests: drv-net: in so_txtime tell apart sw from hw pacing
selftests: drv-net: extend so_txtime with hw offload
Documentation/netlink/specs/rt-link.yaml | 6 +
.../networking/net_cachelines/net_device.rst | 3 +-
drivers/net/ethernet/intel/idpf/idpf.h | 12 ++
drivers/net/ethernet/intel/idpf/idpf_lib.c | 3 +
drivers/net/ethernet/intel/idpf/idpf_txrx.c | 66 +++++++-
drivers/net/ethernet/intel/idpf/idpf_txrx.h | 2 +
.../net/ethernet/intel/idpf/idpf_virtchnl.c | 53 +++++-
include/linux/netdevice.h | 4 +-
include/uapi/linux/if_link.h | 1 +
net/core/rtnetlink.c | 21 +++
net/sched/sch_fq.c | 25 ++-
.../testing/selftests/drivers/net/so_txtime.c | 158 +++++++++++++-----
.../selftests/drivers/net/so_txtime.py | 74 +++++++-
13 files changed, 372 insertions(+), 56 deletions(-)
--
2.55.0.691.gc56d675ccc-goog
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH net-next v6 1/6] net: rtnetlink: add pacing_offload_horizon attribute to net_device
2026-08-13 2:03 [PATCH net-next v6 0/6] hardware pacing offload Willem de Bruijn
@ 2026-08-13 2:03 ` Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 2/6] net_sched: sch_fq: clear past skb->tstamp if offloading pacing Willem de Bruijn
` (4 subsequent siblings)
5 siblings, 0 replies; 8+ messages in thread
From: Willem de Bruijn @ 2026-08-13 2:03 UTC (permalink / raw)
To: netdev
Cc: davem, kuba, edumazet, pabeni, horms, andrew+netdev,
Willem de Bruijn
From: Willem de Bruijn <willemb@google.com>
The 'max_pacing_offload_horizon' field of 'struct net_device' represents
the maximum pacing offload horizon supported by the device.
Add a new field 'pacing_offload_horizon' to store the active pacing
offload horizon.
The new attribute is initialized to 0 (disabled) and can be set from
userspace via RTM_SETLINK up to dev->max_pacing_offload_horizon. This
new default off behavior does not cause regressions, as no driver yet
advertises max_pacing_offload_horizon.
The attribute is omitted from the newlink request spec, because the
value may need to be bound by a device maximum that first needs to be
negotiated with firmware, as is the case for the idpf driver in this
series.
Make both fields u32, to maintain net_device cacheline layout. This
expresses up to 4s of pacing offload, which is sufficient.
Update the YNL specification ('rt-link.yaml') to add the
'pacing-offload-horizon' attribute and include it in link-all-attrs.
Both fields can be read with
python3 tools/net/ynl/pyynl/cli.py \
--spec Documentation/netlink/specs/rt-link.yaml \
--do getlink \
--json '{"ifname": "eth0"}' | grep pacing
And the active horizon set with
python3 tools/net/ynl/pyynl/cli.py \
--spec Documentation/netlink/specs/rt-link.yaml \
--do setlink \
--json '{"ifname": "eth0", "pacing-offload-horizon": 50000000}'
Signed-off-by: Willem de Bruijn <willemb@google.com>
----
Changes
v5 -> v6
- bring back fq_change update
- commit-msg: reword absense of RTM_NEWLINK -> omitted from newlink
---
Documentation/netlink/specs/rt-link.yaml | 6 ++++++
.../networking/net_cachelines/net_device.rst | 3 ++-
include/linux/netdevice.h | 4 +++-
include/uapi/linux/if_link.h | 1 +
net/core/rtnetlink.c | 21 +++++++++++++++++++
net/sched/sch_fq.c | 3 ++-
6 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/Documentation/netlink/specs/rt-link.yaml b/Documentation/netlink/specs/rt-link.yaml
index b80c2ac3ac31..292878ba7e15 100644
--- a/Documentation/netlink/specs/rt-link.yaml
+++ b/Documentation/netlink/specs/rt-link.yaml
@@ -1085,6 +1085,10 @@ attribute-sets:
-
name: tailroom
type: u16
+ -
+ name: pacing-offload-horizon
+ type: uint
+ doc: EDT offload horizon setting for the device (in nsec).
-
name: prop-list-link-attrs
subset-of: link-attrs
@@ -2553,6 +2557,8 @@ operations:
- devlink-port
- gso-ipv4-max-size
- gro-ipv4-max-size
+ - max-pacing-offload-horizon
+ - pacing-offload-horizon
dump:
request:
value: 18
diff --git a/Documentation/networking/net_cachelines/net_device.rst b/Documentation/networking/net_cachelines/net_device.rst
index 512f6d6fa3d8..90e257e815b1 100644
--- a/Documentation/networking/net_cachelines/net_device.rst
+++ b/Documentation/networking/net_cachelines/net_device.rst
@@ -183,7 +183,8 @@ struct devlink_port* devlink_port
struct dpll_pin* dpll_pin
struct hlist_head page_pools
struct dim_irq_moder* irq_moder
-u64 max_pacing_offload_horizon
+u32 max_pacing_offload_horizon
+u32 pacing_offload_horizon
struct_napi_config* napi_config
unsigned_long gro_flush_timeout
u32 napi_defer_hard_irqs
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index db9dce7f0aa6..6155ee8a0f34 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -2133,6 +2133,7 @@ enum netdev_reg_state {
* where the clock is recovered.
*
* @max_pacing_offload_horizon: max EDT offload horizon in nsec.
+ * @pacing_offload_horizon: EDT offload horizon setting in nsec.
* @napi_config: An array of napi_config structures containing per-NAPI
* settings.
* @num_napi_configs: number of allocated NAPI config structs,
@@ -2555,7 +2556,8 @@ struct net_device {
/** @irq_moder: dim parameters used if IS_ENABLED(CONFIG_DIMLIB). */
struct dim_irq_moder *irq_moder;
- u64 max_pacing_offload_horizon;
+ u32 max_pacing_offload_horizon;
+ u32 pacing_offload_horizon;
struct napi_config *napi_config;
u32 num_napi_configs;
u32 napi_defer_hard_irqs;
diff --git a/include/uapi/linux/if_link.h b/include/uapi/linux/if_link.h
index 43cecca49f01..7c1d7754a670 100644
--- a/include/uapi/linux/if_link.h
+++ b/include/uapi/linux/if_link.h
@@ -381,6 +381,7 @@ enum {
IFLA_NETNS_IMMUTABLE,
IFLA_HEADROOM,
IFLA_TAILROOM,
+ IFLA_PACING_OFFLOAD_HORIZON,
__IFLA_MAX
};
diff --git a/net/core/rtnetlink.c b/net/core/rtnetlink.c
index 81c5a6104dea..eeaf953b397f 100644
--- a/net/core/rtnetlink.c
+++ b/net/core/rtnetlink.c
@@ -1396,6 +1396,7 @@ static noinline size_t if_nlmsg_size(const struct net_device *dev,
+ rtnl_devlink_port_size(dev)
+ rtnl_dpll_pin_size()
+ nla_total_size(8) /* IFLA_MAX_PACING_OFFLOAD_HORIZON */
+ + nla_total_size(8) /* IFLA_PACING_OFFLOAD_HORIZON */
+ nla_total_size(2) /* IFLA_HEADROOM */
+ nla_total_size(2) /* IFLA_TAILROOM */
+ rtnl_dev_parent_size(dev)
@@ -2176,6 +2177,8 @@ static int rtnl_fill_ifinfo(struct sk_buff *skb,
READ_ONCE(dev->tso_max_segs)) ||
nla_put_uint(skb, IFLA_MAX_PACING_OFFLOAD_HORIZON,
READ_ONCE(dev->max_pacing_offload_horizon)) ||
+ nla_put_uint(skb, IFLA_PACING_OFFLOAD_HORIZON,
+ READ_ONCE(dev->pacing_offload_horizon)) ||
#ifdef CONFIG_RPS
nla_put_u32(skb, IFLA_NUM_RX_QUEUES,
READ_ONCE(dev->num_rx_queues)) ||
@@ -2344,9 +2347,11 @@ static const struct nla_policy ifla_policy[IFLA_MAX+1] = {
[IFLA_ALLMULTI] = { .type = NLA_REJECT },
[IFLA_GSO_IPV4_MAX_SIZE] = NLA_POLICY_MIN(NLA_U32, MAX_TCP_HEADER + 1),
[IFLA_GRO_IPV4_MAX_SIZE] = { .type = NLA_U32 },
+ [IFLA_MAX_PACING_OFFLOAD_HORIZON] = { .type = NLA_REJECT },
[IFLA_NETNS_IMMUTABLE] = { .type = NLA_REJECT },
[IFLA_HEADROOM] = { .type = NLA_REJECT },
[IFLA_TAILROOM] = { .type = NLA_REJECT },
+ [IFLA_PACING_OFFLOAD_HORIZON] = { .type = NLA_UINT },
};
static const struct nla_policy ifla_info_policy[IFLA_INFO_MAX+1] = {
@@ -2820,6 +2825,13 @@ static int validate_linkmsg(struct net_device *dev, struct nlattr *tb[],
return -EINVAL;
}
+ if (tb[IFLA_PACING_OFFLOAD_HORIZON] &&
+ nla_get_uint(tb[IFLA_PACING_OFFLOAD_HORIZON]) >
+ dev->max_pacing_offload_horizon) {
+ NL_SET_ERR_MSG(extack, "too big pacing_offload_horizon");
+ return -EINVAL;
+ }
+
if (tb[IFLA_AF_SPEC]) {
struct nlattr *af;
int rem, err;
@@ -3337,6 +3349,15 @@ static int do_setlink(const struct sk_buff *skb, struct net_device *dev,
}
}
+ if (tb[IFLA_PACING_OFFLOAD_HORIZON]) {
+ u64 horizon = nla_get_uint(tb[IFLA_PACING_OFFLOAD_HORIZON]);
+
+ if (dev->pacing_offload_horizon ^ horizon) {
+ WRITE_ONCE(dev->pacing_offload_horizon, horizon);
+ status |= DO_SETLINK_MODIFIED;
+ }
+ }
+
if (tb[IFLA_OPERSTATE])
set_operstate(dev, nla_get_u8(tb[IFLA_OPERSTATE]));
diff --git a/net/sched/sch_fq.c b/net/sched/sch_fq.c
index 7cae082a9847..e0f2cd99e3b7 100644
--- a/net/sched/sch_fq.c
+++ b/net/sched/sch_fq.c
@@ -1179,7 +1179,8 @@ static int fq_change(struct Qdisc *sch, struct nlattr *opt,
u64 offload_horizon = (u64)NSEC_PER_USEC *
nla_get_u32(tb[TCA_FQ_OFFLOAD_HORIZON]);
- if (offload_horizon <= qdisc_dev(sch)->max_pacing_offload_horizon) {
+ if (offload_horizon <=
+ READ_ONCE(qdisc_dev(sch)->pacing_offload_horizon)) {
WRITE_ONCE(q->offload_horizon, offload_horizon);
} else {
NL_SET_ERR_MSG_MOD(extack, "invalid offload_horizon");
--
2.55.0.691.gc56d675ccc-goog
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net-next v6 2/6] net_sched: sch_fq: clear past skb->tstamp if offloading pacing
2026-08-13 2:03 [PATCH net-next v6 0/6] hardware pacing offload Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 1/6] net: rtnetlink: add pacing_offload_horizon attribute to net_device Willem de Bruijn
@ 2026-08-13 2:03 ` Willem de Bruijn
2026-08-13 5:12 ` Eric Dumazet
2026-08-13 2:03 ` [PATCH net-next v6 3/6] idpf: support pacing offload Willem de Bruijn
` (3 subsequent siblings)
5 siblings, 1 reply; 8+ messages in thread
From: Willem de Bruijn @ 2026-08-13 2:03 UTC (permalink / raw)
To: netdev
Cc: davem, kuba, edumazet, pabeni, horms, andrew+netdev,
Willem de Bruijn
From: Willem de Bruijn <willemb@google.com>
When hardware offload is enabled, FQ will forward packets to the
netdevice for pacing. The device has to test that skb->tstamp is
in the future.
Avoid this cost for packets whose txtime has already passed, by
clearing skb->tstamp.
Also clear slightly into the future, for EDT timestamps that are
so close to now that they fall within a reasonable normal Tx
latency. This slack is set to 400 nsec.
Also disable timer drift logic when offload is enabled, because
time_next_packet can exceed now causing a negative value.
Signed-off-by: Willem de Bruijn <willemb@google.com>
---
Changes
v5 -> v6
- switch to skb_set_delivery_time to also clear SKB_CLOCK_REALTIME
- add fq_offload_slack_ns
v3 -> v4
- also reset tstamp_type
- minor: initialize time_next_packet for more obvious correctness
Sashiko, ignore pre-existing issues. In particular, effects on
non-EDT packets and when queue or sk rate limit is set.
Sashiko, pacing offload is an optimization. Ignore that some packets
may not get offloaded, e.g., when txtime is a few usec in the future.
Claude suggests to only clear delivery time in one location
in fq_dequeue. Unfortunately the separate fastpath location is needed
as that avoids computing now in fq_dequeue for these fastpath packets.
---
net/sched/sch_fq.c | 22 ++++++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/net/sched/sch_fq.c b/net/sched/sch_fq.c
index e0f2cd99e3b7..e7730fb04264 100644
--- a/net/sched/sch_fq.c
+++ b/net/sched/sch_fq.c
@@ -159,6 +159,9 @@ struct fq_sched_data {
u64 stat_allocation_errors;
};
+/* EDT timestamps to clear beyond now. */
+static const int fq_offload_slack_ns = 400;
+
/* return the i-th 2-bit value ("crumb") */
static u8 fq_prio2band(const u8 *prio2band, unsigned int prio)
{
@@ -399,6 +402,11 @@ static struct fq_flow *fq_classify(struct Qdisc *sch, struct sk_buff *skb,
READ_ONCE(sk->sk_pacing_status) != SK_PACING_FQ)
smp_store_release(&sk->sk_pacing_status,
SK_PACING_FQ);
+
+ if (q->offload_horizon &&
+ fq_skb_cb(skb)->time_to_send <= now + fq_offload_slack_ns)
+ skb_set_delivery_time(skb, 0, 0);
+
return &q->internal;
}
@@ -707,6 +715,7 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
struct fq_sched_data *q = qdisc_priv(sch);
struct fq_perband_flows *pband;
struct fq_flow_head *head;
+ u64 time_next_packet = 0;
struct sk_buff *skb;
struct fq_flow *f;
unsigned long rate;
@@ -721,7 +730,7 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
if (skb) {
q->internal.qlen--;
fq_dequeue_skb(sch, &q->internal, skb);
- goto out;
+ return skb;
}
now = ktime_get_ns();
@@ -758,8 +767,8 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
skb = fq_peek(f);
if (skb) {
- u64 time_next_packet = max_t(u64, fq_skb_cb(skb)->time_to_send,
- f->time_next_packet);
+ time_next_packet = max_t(u64, fq_skb_cb(skb)->time_to_send,
+ f->time_next_packet);
if (now + q->offload_horizon < time_next_packet) {
head->first = f->next;
@@ -828,11 +837,16 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
* f->time_next_packet was set when prior packet was sent,
* and current time (@now) can be too late by tens of us.
*/
- if (f->time_next_packet)
+ if (f->time_next_packet && f->time_next_packet < now)
len -= min(len/2, now - f->time_next_packet);
f->time_next_packet = now + len;
}
+
out:
+ if (q->offload_horizon &&
+ time_next_packet && time_next_packet <= now + fq_offload_slack_ns)
+ skb_set_delivery_time(skb, 0, 0);
+
return skb;
}
--
2.55.0.691.gc56d675ccc-goog
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net-next v6 3/6] idpf: support pacing offload
2026-08-13 2:03 [PATCH net-next v6 0/6] hardware pacing offload Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 1/6] net: rtnetlink: add pacing_offload_horizon attribute to net_device Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 2/6] net_sched: sch_fq: clear past skb->tstamp if offloading pacing Willem de Bruijn
@ 2026-08-13 2:03 ` Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 4/6] selftests: drv-net: refactor so_txtime errqueue handling Willem de Bruijn
` (2 subsequent siblings)
5 siblings, 0 replies; 8+ messages in thread
From: Willem de Bruijn @ 2026-08-13 2:03 UTC (permalink / raw)
To: netdev
Cc: davem, kuba, edumazet, pabeni, horms, andrew+netdev,
Willem de Bruijn, Tony Nguyen, Przemek Kitszel, Joshua A Hay
From: Willem de Bruijn <willemb@google.com>
If skb->tstamp is in the future, program this future delivery txtime
in the transmit descriptor.
TCP pacing offload is only offloaded if SK_PACING_FQ is negotiated and
the FQ offload_horizon is configured. But device support for pacing
offload must be more robust: it can also be reached through SO_TXTIME.
Bounds check txtime. Only packets with timestamp between now and the
horizon (pacing_offload_horizon) are offloaded.
Negotiate the feature with the device using virtchnl. Support is
conditional on
- splitq mode, where tx and tx completion queues are separate, so
completions can be returned out of order.
- flow scheduling mode, where completions can arrive out of order.
- PTP to ensure the NIC clock is synced to CLOCK_TAI.
Do not explicitly check for these preconditions. Trust the firmware to
only advertise EDT when all are met. These features are negotiated
per adapter, but expect all vports to uniformly request splitq
(req_[rt]x_splitq) and flow scheduling (flow_sch_en) when available.
In general, firmware response is trusted to be correct. If not, EDT
would be the least of the worries. In particular, granularity is
trusted to be a power of two, no check needed. And time_horizon_ns
is a range expressible given the chosen granularity.
Packets beyond the horizon are sent immediately with the overflow bit
set.
On device reset, dev->pacing_offload_horizon, fq offload_horizon and
granularity are not re-negotiated. It is safe to assume that firmware
does not change these EDT capabilities across resets.
Must not be called from netpoll due to ktime_get. But netpoll does not
generate packets with EDT, so no explicit test is needed.
Do not fail device initialization on EDT init error. Log an error, but
continue without EDT, similar to PTP.
Cc: Tony Nguyen <anthony.l.nguyen@intel.com>
Cc: Przemek Kitszel <przemyslaw.kitszel@intel.com>
Cc: Joshua A Hay <joshua.a.hay@intel.com>
Signed-off-by: Willem de Bruijn <willemb@google.com>
---
Changes
v5 -> v6
- move EDT caps from virtchnl2_edt_caps_ilog2 to idpf_edt_caps_ilog2
- do not fail device init if EDT init failed, same as PTP
- replace netpoll branch with commit-msg comment
- add commit-msg comment about device reset
- add commit-msg about behavior beyond horizon
- add commit-msg about trusting EDT capability response
v4 -> v5
- fix kdoc: idpf.h:738 struct member 'edt_caps' not described..
v3 -> v4
- add EDT virtchnl negotiation
- move endianness fix to its own iwl-net patch
- update commit-msg: check is against pacing_offload_horizon, not max_..
- simplify assignment, avoid the le32 assignment in favor of u8 inits
- replace open coded constant with IDPF_TXD_FLOW_SCH_HORIZON_OVERFLOW_M
- (minor) fix 24b timestamp comment: is 23b + overflow bit
- (minor) remove unused parameter
v2 -> v3
- use READ_ONCE and read pacing_offload_horizon only once
v1 -> v2
- move special zero case up and return early
Sashiko, ignore pre-existing issues.
Sashiko, ignore that idpf_tx_splitq_set_txtime may have a benign race
by calling ktime_mono_to_any twice to get TAI to REALTIME offset.
---
drivers/net/ethernet/intel/idpf/idpf.h | 12 ++++
drivers/net/ethernet/intel/idpf/idpf_lib.c | 3 +
drivers/net/ethernet/intel/idpf/idpf_txrx.c | 66 ++++++++++++++++++-
drivers/net/ethernet/intel/idpf/idpf_txrx.h | 2 +
.../net/ethernet/intel/idpf/idpf_virtchnl.c | 53 ++++++++++++++-
5 files changed, 134 insertions(+), 2 deletions(-)
diff --git a/drivers/net/ethernet/intel/idpf/idpf.h b/drivers/net/ethernet/intel/idpf/idpf.h
index ec1b75f039bb..4eb2fbe2a62b 100644
--- a/drivers/net/ethernet/intel/idpf/idpf.h
+++ b/drivers/net/ethernet/intel/idpf/idpf.h
@@ -605,6 +605,16 @@ struct idpf_vport_config {
DECLARE_BITMAP(flags, IDPF_VPORT_CONFIG_FLAGS_NBITS);
};
+/**
+ * struct idpf_edt_caps_ilog2 - Host parsed EDT capabilities.
+ * @time_horizon_ns: Total time window in nanoseconds.
+ * @tstamp_granularity_pow2: Log2 of timestamp granularity in nanoseconds.
+ */
+struct idpf_edt_caps_ilog2 {
+ u32 time_horizon_ns;
+ u8 tstamp_granularity_pow2;
+};
+
struct idpf_vc_xn_manager;
#define idpf_for_each_vport(adapter, iter) \
@@ -658,6 +668,7 @@ struct idpf_vc_xn_manager;
* @stats_task: Periodic statistics retrieval task
* @stats_wq: Workqueue for statistics task
* @caps: Negotiated capabilities with device
+ * @edt_caps: Negotiated EDT capabilities with device
* @vcxn_mngr: Virtchnl transaction manager
* @dev_ops: See idpf_dev_ops
* @cdev_info: IDC core device info pointer
@@ -720,6 +731,7 @@ struct idpf_adapter {
struct delayed_work stats_task;
struct workqueue_struct *stats_wq;
struct virtchnl2_get_capabilities caps;
+ struct idpf_edt_caps_ilog2 edt_caps;
struct idpf_vc_xn_manager *vcxn_mngr;
struct idpf_dev_ops dev_ops;
diff --git a/drivers/net/ethernet/intel/idpf/idpf_lib.c b/drivers/net/ethernet/intel/idpf/idpf_lib.c
index bb81e620c5c8..106e59dd5fad 100644
--- a/drivers/net/ethernet/intel/idpf/idpf_lib.c
+++ b/drivers/net/ethernet/intel/idpf/idpf_lib.c
@@ -888,6 +888,9 @@ static int idpf_cfg_netdev(struct idpf_vport *vport)
netdev->min_mtu = ETH_MIN_MTU;
netdev->max_mtu = vport->max_mtu;
+ if (adapter->edt_caps.time_horizon_ns)
+ netdev->max_pacing_offload_horizon = adapter->edt_caps.time_horizon_ns;
+
dflt_features = NETIF_F_SG |
NETIF_F_HIGHDMA;
diff --git a/drivers/net/ethernet/intel/idpf/idpf_txrx.c b/drivers/net/ethernet/intel/idpf/idpf_txrx.c
index 99fcd8e298d6..c020ad1e2265 100644
--- a/drivers/net/ethernet/intel/idpf/idpf_txrx.c
+++ b/drivers/net/ethernet/intel/idpf/idpf_txrx.c
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (C) 2023 Intel Corporation */
+#include <linux/netpoll.h>
+
#include "idpf.h"
#include "idpf_ptp.h"
#include "idpf_virtchnl.h"
@@ -2408,7 +2410,12 @@ void idpf_tx_splitq_build_flow_desc(union idpf_tx_flex_desc *desc,
struct idpf_tx_splitq_params *params,
u16 td_cmd, u16 size)
{
- *(u32 *)&desc->flow.qw1.cmd_dtype = (u8)(params->dtype | td_cmd);
+ desc->flow.qw1.cmd_dtype = (u8)(params->dtype | td_cmd);
+
+ desc->flow.qw1.ts[0] = params->offload.desc_ts[0];
+ desc->flow.qw1.ts[1] = params->offload.desc_ts[1];
+ desc->flow.qw1.ts[2] = params->offload.desc_ts[2];
+
desc->flow.qw1.rxr_bufsize = cpu_to_le16((u16)size);
desc->flow.qw1.compl_tag = cpu_to_le16(params->compl_tag);
}
@@ -3011,6 +3018,59 @@ static bool idpf_tx_splitq_need_re(struct idpf_tx_queue *tx_q)
return gap >= IDPF_TX_SPLITQ_RE_MIN_GAP;
}
+static void idpf_tx_splitq_set_txtime(const struct sk_buff *skb,
+ struct idpf_tx_splitq_params *tx_params)
+{
+ struct idpf_netdev_priv *np = netdev_priv(skb->dev);
+ u64 ts, now, horizon;
+
+ horizon = READ_ONCE(skb->dev->pacing_offload_horizon);
+ if (!horizon)
+ return;
+
+ switch (skb->tstamp_type) {
+ case SKB_CLOCK_REALTIME:
+ ts = ktime_to_ns(ktime_add(skb->tstamp,
+ ktime_mono_to_any(0, TK_OFFS_TAI) -
+ ktime_mono_to_any(0, TK_OFFS_REAL)));
+ break;
+ case SKB_CLOCK_MONOTONIC:
+ ts = ktime_to_ns(ktime_mono_to_any(skb->tstamp, TK_OFFS_TAI));
+ break;
+ case SKB_CLOCK_TAI:
+ ts = ktime_to_ns(skb->tstamp);
+ break;
+ default:
+ WARN_ON_ONCE(1);
+ return;
+ }
+
+ now = ktime_get_clocktai_ns();
+ if (ts < now)
+ return;
+
+ /* beyond offload horizon? set overflow bit only */
+ if (ts > now + horizon) {
+ tx_params->offload.desc_ts[2] =
+ IDPF_TXD_FLOW_SCH_HORIZON_OVERFLOW_M;
+ return;
+ }
+
+ ts >>= np->adapter->edt_caps.tstamp_granularity_pow2;
+
+ /* 0 is valid 23b timestamp, but also means field unset.
+ * Increase by one to avoid this case
+ */
+ if ((ts & 0x7fffff) == 0) {
+ tx_params->offload.desc_ts[0] = 1;
+ return;
+ }
+
+ tx_params->offload.desc_ts[0] = ts & 0xff;
+ tx_params->offload.desc_ts[1] = (ts >> 8) & 0xff;
+ tx_params->offload.desc_ts[2] = ((ts >> 16) & 0x7f);
+}
+
/**
* idpf_tx_splitq_frame - Sends buffer on Tx ring using flex descriptors
* @skb: send buffer
@@ -3097,6 +3157,10 @@ static netdev_tx_t idpf_tx_splitq_frame(struct sk_buff *skb,
tx_params.dtype = IDPF_TX_DESC_DTYPE_FLEX_FLOW_SCHE;
tx_params.eop_cmd = IDPF_TXD_FLEX_FLOW_CMD_EOP;
+
+ if (skb->tstamp)
+ idpf_tx_splitq_set_txtime(skb, &tx_params);
+
/* Set the RE bit periodically to "clean" the descriptor ring */
if (idpf_tx_splitq_need_re(tx_q)) {
tx_params.eop_cmd |= IDPF_TXD_FLEX_FLOW_CMD_RE;
diff --git a/drivers/net/ethernet/intel/idpf/idpf_txrx.h b/drivers/net/ethernet/intel/idpf/idpf_txrx.h
index 908dfa28674e..86e881c697cc 100644
--- a/drivers/net/ethernet/intel/idpf/idpf_txrx.h
+++ b/drivers/net/ethernet/intel/idpf/idpf_txrx.h
@@ -161,6 +161,7 @@ union idpf_tx_flex_desc {
* @tso_segs: Number of segments to be sent
* @tso_hdr_len: Length of headers to be duplicated
* @td_cmd: Command field to be inserted into descriptor
+ * @desc_ts: Flow scheduling offload timestamp
*/
struct idpf_tx_offload_params {
u32 tx_flags;
@@ -174,6 +175,7 @@ struct idpf_tx_offload_params {
u16 tso_hdr_len;
u16 td_cmd;
+ u8 desc_ts[3];
};
/**
diff --git a/drivers/net/ethernet/intel/idpf/idpf_virtchnl.c b/drivers/net/ethernet/intel/idpf/idpf_virtchnl.c
index 8bd6cca64c9b..fd3550cc248d 100644
--- a/drivers/net/ethernet/intel/idpf/idpf_virtchnl.c
+++ b/drivers/net/ethernet/intel/idpf/idpf_virtchnl.c
@@ -2,6 +2,7 @@
/* Copyright (C) 2023 Intel Corporation */
#include <linux/export.h>
+#include <linux/log2.h>
#include <net/libeth/rx.h>
#include "idpf.h"
@@ -1001,7 +1002,8 @@ static int idpf_send_get_caps_msg(struct idpf_adapter *adapter)
VIRTCHNL2_CAP_SPLITQ_QSCHED |
VIRTCHNL2_CAP_PROMISC |
VIRTCHNL2_CAP_LOOPBACK |
- VIRTCHNL2_CAP_PTP);
+ VIRTCHNL2_CAP_PTP |
+ VIRTCHNL2_CAP_EDT);
xn_params.vc_op = VIRTCHNL2_OP_GET_CAPS;
xn_params.send_buf.iov_base = ∩︀
@@ -1019,6 +1021,49 @@ static int idpf_send_get_caps_msg(struct idpf_adapter *adapter)
return 0;
}
+/**
+ * idpf_send_get_edt_caps_msg - Send virtchnl get EDT caps msg
+ * @adapter: Driver specific private struct
+ *
+ * Return: 0 on success or error code on failure.
+ */
+static int idpf_send_get_edt_caps_msg(struct idpf_adapter *adapter)
+{
+ struct virtchnl2_edt_caps caps = {};
+ struct idpf_vc_xn_params xn_params = {
+ .vc_op = VIRTCHNL2_OP_GET_EDT_CAPS,
+ .send_buf = {
+ .iov_base = &caps,
+ .iov_len = sizeof(caps),
+ },
+ .recv_buf = {
+ .iov_base = &caps,
+ .iov_len = sizeof(caps),
+ },
+ .timeout_ms = IDPF_VC_XN_DEFAULT_TIMEOUT_MSEC,
+ };
+ ssize_t reply_sz;
+ u64 gran_ns, horizon_ns;
+
+ reply_sz = idpf_vc_xn_exec(adapter, &xn_params);
+ if (reply_sz < 0)
+ return reply_sz;
+ if (reply_sz < sizeof(caps))
+ return -EIO;
+
+ horizon_ns = le64_to_cpu(caps.time_horizon_ns);
+ if (horizon_ns > U32_MAX) {
+ dev_warn(&adapter->pdev->dev, "EDT horizon exceeds U32\n");
+ return -EINVAL;
+ }
+
+ adapter->edt_caps.time_horizon_ns = horizon_ns;
+ gran_ns = le64_to_cpu(caps.tstamp_granularity_ns);
+ adapter->edt_caps.tstamp_granularity_pow2 = gran_ns ? ilog2(gran_ns) : 9;
+
+ return 0;
+}
+
/**
* idpf_send_get_lan_memory_regions - Send virtchnl get LAN memory regions msg
* @adapter: Driver specific private struct
@@ -3547,6 +3592,12 @@ int idpf_vc_core_init(struct idpf_adapter *adapter)
}
}
+ if (idpf_is_cap_ena(adapter, IDPF_OTHER_CAPS, VIRTCHNL2_CAP_EDT)) {
+ err = idpf_send_get_edt_caps_msg(adapter);
+ if (err)
+ dev_err(&adapter->pdev->dev, "EDT init failed, err=%d\n", err);
+ }
+
err = idpf_map_lan_mmio_regs(adapter);
if (err) {
dev_err(&adapter->pdev->dev, "Failed to map BAR0 region(s): %d\n",
--
2.55.0.691.gc56d675ccc-goog
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net-next v6 4/6] selftests: drv-net: refactor so_txtime errqueue handling
2026-08-13 2:03 [PATCH net-next v6 0/6] hardware pacing offload Willem de Bruijn
` (2 preceding siblings ...)
2026-08-13 2:03 ` [PATCH net-next v6 3/6] idpf: support pacing offload Willem de Bruijn
@ 2026-08-13 2:03 ` Willem de Bruijn
2026-08-13 2:04 ` [PATCH net-next v6 5/6] selftests: drv-net: in so_txtime tell apart sw from hw pacing Willem de Bruijn
2026-08-13 2:04 ` [PATCH net-next v6 6/6] selftests: drv-net: extend so_txtime with hw offload Willem de Bruijn
5 siblings, 0 replies; 8+ messages in thread
From: Willem de Bruijn @ 2026-08-13 2:03 UTC (permalink / raw)
To: netdev
Cc: davem, kuba, edumazet, pabeni, horms, andrew+netdev,
Willem de Bruijn
From: Willem de Bruijn <willemb@google.com>
Prepare error queue handling for upcoming SO_EE_ORIGIN_TIMESTAMPING
messages in the next patch in this series.
Convert do_recv_errqueue_timeout into dispatcher do_recv_errqueue
and move SO_EE_ORIGIN_TXTIME specific code into a separate helper.
This will make the next patch a lot more readable.
No functional changes.
Signed-off-by: Willem de Bruijn <willemb@google.com>
--
Changes
v3 -> v4
- rename func to do_recv_errqueue_txtime
---
.../testing/selftests/drivers/net/so_txtime.c | 66 ++++++++++---------
1 file changed, 36 insertions(+), 30 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/so_txtime.c b/tools/testing/selftests/drivers/net/so_txtime.c
index 55a386f3d1b9..f6f3253ed174 100644
--- a/tools/testing/selftests/drivers/net/so_txtime.c
+++ b/tools/testing/selftests/drivers/net/so_txtime.c
@@ -158,7 +158,40 @@ static void do_recv_verify_empty(int fdr)
error(1, 0, "recv: not empty as expected (%d, %d)", ret, errno);
}
-static int do_recv_errqueue_timeout(int fdt)
+static int do_recv_errqueue_txtime(struct sock_extended_err *err,
+ const char payload_char)
+{
+ const char *reason = NULL;
+ int64_t tstamp = 0;
+
+ switch (err->ee_errno) {
+ case ECANCELED:
+ if (err->ee_code != SO_EE_CODE_TXTIME_MISSED)
+ error(1, 0, "errqueue: unknown ECANCELED %u\n",
+ err->ee_code);
+ reason = "missed txtime";
+ break;
+ case EINVAL:
+ if (err->ee_code != SO_EE_CODE_TXTIME_INVALID_PARAM)
+ error(1, 0, "errqueue: unknown EINVAL %u\n",
+ err->ee_code);
+ reason = "invalid txtime";
+ break;
+ default:
+ error(1, 0, "errqueue: errno %u code %u\n",
+ err->ee_errno, err->ee_code);
+ }
+
+ tstamp = ((int64_t)err->ee_data) << 32 | err->ee_info;
+ tstamp -= (int64_t)glob_tstart;
+ tstamp /= 1000 * 1000;
+ fprintf(stderr, "send: pkt %c at %" PRId64 "ms dropped: %s\n",
+ payload_char, tstamp, reason);
+
+ return 1;
+}
+
+static int do_recv_errqueue(int fdt)
{
char control[CMSG_SPACE(sizeof(struct sock_extended_err)) +
CMSG_SPACE(sizeof(struct sockaddr_in6))] = {0};
@@ -169,7 +202,6 @@ static int do_recv_errqueue_timeout(int fdt)
struct msghdr msg = {0};
struct iovec iov = {0};
struct cmsghdr *cm;
- int64_t tstamp = 0;
iov.iov_base = data;
iov.iov_len = sizeof(data);
@@ -181,8 +213,6 @@ static int do_recv_errqueue_timeout(int fdt)
msg.msg_controllen = sizeof(control);
while (1) {
- const char *reason = NULL;
-
ret = recvmsg(fdt, &msg, MSG_ERRQUEUE);
if (ret == -1 && errno == EAGAIN)
break;
@@ -201,33 +231,9 @@ static int do_recv_errqueue_timeout(int fdt)
if (err->ee_origin != SO_EE_ORIGIN_TXTIME)
error(1, 0, "errqueue: origin 0x%x\n", err->ee_origin);
- switch (err->ee_errno) {
- case ECANCELED:
- if (err->ee_code != SO_EE_CODE_TXTIME_MISSED)
- error(1, 0, "errqueue: unknown ECANCELED %u\n",
- err->ee_code);
- reason = "missed txtime";
- break;
- case EINVAL:
- if (err->ee_code != SO_EE_CODE_TXTIME_INVALID_PARAM)
- error(1, 0, "errqueue: unknown EINVAL %u\n",
- err->ee_code);
- reason = "invalid txtime";
- break;
- default:
- error(1, 0, "errqueue: errno %u code %u\n",
- err->ee_errno, err->ee_code);
- }
-
- tstamp = ((int64_t) err->ee_data) << 32 | err->ee_info;
- tstamp -= (int64_t) glob_tstart;
- tstamp /= 1000 * 1000;
- fprintf(stderr, "send: pkt %c at %" PRId64 "ms dropped: %s\n",
- data[ret - 1], tstamp, reason);
-
+ num_tstamp += do_recv_errqueue_txtime(err, data[ret - 1]);
msg.msg_flags = 0;
msg.msg_controllen = sizeof(control);
- num_tstamp++;
}
return num_tstamp;
@@ -245,7 +251,7 @@ static void recv_errqueue_msgs(int fdt)
error(1, errno, "poll");
if (ret && (pfd.revents & POLLERR))
- num_tstamp += do_recv_errqueue_timeout(fdt);
+ num_tstamp += do_recv_errqueue(fdt);
if (num_tstamp == cfg_num_pkt)
break;
--
2.55.0.691.gc56d675ccc-goog
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net-next v6 5/6] selftests: drv-net: in so_txtime tell apart sw from hw pacing
2026-08-13 2:03 [PATCH net-next v6 0/6] hardware pacing offload Willem de Bruijn
` (3 preceding siblings ...)
2026-08-13 2:03 ` [PATCH net-next v6 4/6] selftests: drv-net: refactor so_txtime errqueue handling Willem de Bruijn
@ 2026-08-13 2:04 ` Willem de Bruijn
2026-08-13 2:04 ` [PATCH net-next v6 6/6] selftests: drv-net: extend so_txtime with hw offload Willem de Bruijn
5 siblings, 0 replies; 8+ messages in thread
From: Willem de Bruijn @ 2026-08-13 2:04 UTC (permalink / raw)
To: netdev
Cc: davem, kuba, edumazet, pabeni, horms, andrew+netdev,
Willem de Bruijn
From: Willem de Bruijn <willemb@google.com>
Detect software pacing in so_txtime.c using SO_TIMESTAMPING.
If '-H' (hw) is passed
1. measure sw tx delay with SOF_TIMESTAMPING_TX_SOFTWARE, and
2. fail if delay exceeds a threshold, indicating pacing
Also extend while condition to account for possible variance.
This applies to all tests, not just the new '-H' variants.
Also reorder getopt parameters to make them alphabetical.
Signed-off-by: Willem de Bruijn <willemb@google.com>
---
Changes
v5 -> v6
- initialize tdeliver_max to now (glob_tstart),
as otherwise not set for -H tests without delay
v3 -> v4
- rename -h to -H to avoid aliasing implicit getopt -h
- count sw timestamps to verify that they were actually checked
- (minor) do not ask for SOF_TIMESTAMPING_OPT_ID as not used
- (minor) remove unused parameter
Reading timestamps from the error queue is never particularly slow,
so errors are always counted, not only if !KSFT_MACHINE_SLOW.
---
.../testing/selftests/drivers/net/so_txtime.c | 94 +++++++++++++++----
1 file changed, 78 insertions(+), 16 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/so_txtime.c b/tools/testing/selftests/drivers/net/so_txtime.c
index f6f3253ed174..c40e24cdef69 100644
--- a/tools/testing/selftests/drivers/net/so_txtime.c
+++ b/tools/testing/selftests/drivers/net/so_txtime.c
@@ -42,8 +42,10 @@ static bool cfg_machine_slow;
static uint64_t cfg_start_time_ns;
static int cfg_mark;
static bool cfg_rx;
+static bool cfg_verify_hw_offload;
static uint64_t glob_tstart;
+static uint64_t glob_tstart_real;
static uint64_t tdeliver_max;
static int errors;
@@ -191,12 +193,32 @@ static int do_recv_errqueue_txtime(struct sock_extended_err *err,
return 1;
}
-static int do_recv_errqueue(int fdt)
+static int do_recv_errqueue_timestamping(struct scm_timestamping *tss)
+{
+ int64_t ts;
+
+ ts = tss->ts[0].tv_sec * 1000ULL * 1000 * 1000;
+ ts += tss->ts[0].tv_nsec;
+ ts -= glob_tstart_real;
+ ts /= 1000;
+
+ if (ts > cfg_variance_us) {
+ fprintf(stderr, "sw delay %" PRId64 "us exceeds bounds\n", ts);
+ if (!cfg_machine_slow)
+ errors++;
+ }
+
+ return 1;
+}
+
+static int do_recv_errqueue(int fdt, int *num_ts)
{
char control[CMSG_SPACE(sizeof(struct sock_extended_err)) +
+ CMSG_SPACE(sizeof(struct scm_timestamping)) +
CMSG_SPACE(sizeof(struct sockaddr_in6))] = {0};
char data[sizeof(struct ethhdr) + sizeof(struct ipv6hdr) +
sizeof(struct udphdr) + 1];
+ struct scm_timestamping *tss;
struct sock_extended_err *err;
int ret, num_tstamp = 0;
struct msghdr msg = {0};
@@ -222,16 +244,29 @@ static int do_recv_errqueue(int fdt)
error(1, 0, "errqueue: flags 0x%x\n", msg.msg_flags);
cm = CMSG_FIRSTHDR(&msg);
+ tss = NULL;
+
+ if (cm->cmsg_level == SOL_SOCKET &&
+ cm->cmsg_type == SCM_TIMESTAMPING) {
+ tss = (void *)CMSG_DATA(cm);
+ cm = CMSG_NXTHDR(&msg, cm);
+ if (!cm)
+ error(1, 0, "timestamp missing ip err\n");
+ }
+
if (cm->cmsg_level != cfg_errq_level ||
cm->cmsg_type != cfg_errq_type)
error(1, 0, "errqueue: type 0x%x.0x%x\n",
cm->cmsg_level, cm->cmsg_type);
err = (struct sock_extended_err *)CMSG_DATA(cm);
- if (err->ee_origin != SO_EE_ORIGIN_TXTIME)
+ if (err->ee_origin == SO_EE_ORIGIN_TXTIME)
+ num_tstamp += do_recv_errqueue_txtime(err, data[ret - 1]);
+ else if (err->ee_origin == SO_EE_ORIGIN_TIMESTAMPING && tss)
+ *num_ts += do_recv_errqueue_timestamping(tss);
+ else
error(1, 0, "errqueue: origin 0x%x\n", err->ee_origin);
- num_tstamp += do_recv_errqueue_txtime(err, data[ret - 1]);
msg.msg_flags = 0;
msg.msg_controllen = sizeof(control);
}
@@ -243,7 +278,7 @@ static void recv_errqueue_msgs(int fdt)
{
struct pollfd pfd = { .fd = fdt, .events = POLLERR };
const int timeout_ms = 10;
- int ret, num_tstamp = 0;
+ int ret, num_tstamp = 0, num_ts = 0;
do {
ret = poll(&pfd, 1, timeout_ms);
@@ -251,12 +286,19 @@ static void recv_errqueue_msgs(int fdt)
error(1, errno, "poll");
if (ret && (pfd.revents & POLLERR))
- num_tstamp += do_recv_errqueue(fdt);
+ num_tstamp += do_recv_errqueue(fdt, &num_ts);
- if (num_tstamp == cfg_num_pkt)
+ if (num_tstamp == cfg_num_pkt || num_ts == cfg_num_pkt)
break;
- } while (gettime_ns(cfg_clockid) < tdeliver_max);
+ } while (gettime_ns(cfg_clockid) <
+ tdeliver_max + (cfg_variance_us * 1000));
+
+ if (cfg_verify_hw_offload && num_ts != cfg_num_pkt) {
+ fprintf(stderr, "missing timestamps: expected %d, got %d\n",
+ cfg_num_pkt, num_ts);
+ errors++;
+ }
}
static void start_time_wait(void)
@@ -301,6 +343,17 @@ static void setsockopt_txtime(int fd)
error(1, 0, "getsockopt txtime: mismatch");
}
+static void setsockopt_txtimestamping(int fd)
+{
+ int val = SOF_TIMESTAMPING_TX_SOFTWARE |
+ SOF_TIMESTAMPING_SOFTWARE |
+ SOF_TIMESTAMPING_OPT_TSONLY;
+
+ if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING,
+ &val, sizeof(val)))
+ error(1, errno, "setsockopt timestamping");
+}
+
static int setup_tx(struct sockaddr *addr, socklen_t alen)
{
int fd;
@@ -314,6 +367,9 @@ static int setup_tx(struct sockaddr *addr, socklen_t alen)
setsockopt_txtime(fd);
+ if (cfg_verify_hw_offload)
+ setsockopt_txtimestamping(fd);
+
if (cfg_mark &&
setsockopt(fd, SOL_SOCKET, SO_MARK, &cfg_mark, sizeof(cfg_mark)))
error(1, errno, "setsockopt mark");
@@ -354,6 +410,8 @@ static void do_test_tx(struct sockaddr *addr, socklen_t alen)
start_time_wait();
glob_tstart = gettime_ns(cfg_clockid);
+ glob_tstart_real = gettime_ns(CLOCK_REALTIME);
+ tdeliver_max = glob_tstart;
for (i = 0; i < cfg_num_pkt; i++)
do_send_one(fdt, &cfg_buf[i]);
@@ -446,10 +504,11 @@ static void usage(const char *progname)
" -6 only IPv6\n"
" -c <clock> monotonic or tai (default)\n"
" -D <addr> destination IP address (server)\n"
- " -S <addr> source IP address (client)\n"
+ " -H verify hardware offload (tx)\n"
+ " -m <mark> socket mark\n"
" -r run rx mode\n"
+ " -S <addr> source IP address (client)\n"
" -t <nsec> start time (UTC nanoseconds)\n"
- " -m <mark> socket mark\n"
"\n",
progname);
exit(1);
@@ -461,7 +520,7 @@ static void parse_opts(int argc, char **argv)
int domain = PF_UNSPEC;
int c;
- while ((c = getopt(argc, argv, "46c:S:D:rt:m:")) != -1) {
+ while ((c = getopt(argc, argv, "46c:D:Hm:rS:t:")) != -1) {
switch (c) {
case '4':
if (domain != PF_UNSPEC)
@@ -488,21 +547,24 @@ static void parse_opts(int argc, char **argv)
else
error(1, 0, "unknown clock id %s", optarg);
break;
- case 'S':
- saddr = optarg;
- break;
case 'D':
daddr = optarg;
break;
+ case 'H':
+ cfg_verify_hw_offload = true;
+ break;
+ case 'm':
+ cfg_mark = strtol(optarg, NULL, 0);
+ break;
case 'r':
cfg_rx = true;
break;
+ case 'S':
+ saddr = optarg;
+ break;
case 't':
cfg_start_time_ns = strtoll(optarg, NULL, 0);
break;
- case 'm':
- cfg_mark = strtol(optarg, NULL, 0);
- break;
default:
usage(argv[0]);
}
--
2.55.0.691.gc56d675ccc-goog
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net-next v6 6/6] selftests: drv-net: extend so_txtime with hw offload
2026-08-13 2:03 [PATCH net-next v6 0/6] hardware pacing offload Willem de Bruijn
` (4 preceding siblings ...)
2026-08-13 2:04 ` [PATCH net-next v6 5/6] selftests: drv-net: in so_txtime tell apart sw from hw pacing Willem de Bruijn
@ 2026-08-13 2:04 ` Willem de Bruijn
5 siblings, 0 replies; 8+ messages in thread
From: Willem de Bruijn @ 2026-08-13 2:04 UTC (permalink / raw)
To: netdev
Cc: davem, kuba, edumazet, pabeni, horms, andrew+netdev,
Willem de Bruijn
From: Willem de Bruijn <willemb@google.com>
Add two pacing hardware offload variants
1. one that uses FQ to safely offload when within bounds.
2. one that uses pfifo_fast and thus forwards all packets.
Verify that the packets are paced in hardware with new flag '-H'.
Also increase rcvtimeout significantly to reduce flakiness. Especially
for the new beyond_hw_horizon test, which is close to the 100ms limit.
But update recv_verify_empty to take MSG_DONTWAIT. That last empty
check must not delay each testcase by the receive timeout.
Hardware pacing offload can complete packets out of order. So the
reverse_order test is expected to pass with pfifo_fast too.
Do not test ETF, which does not change its dequeue behavior based on
pacing_offload_horizon.
Signed-off-by: Willem de Bruijn <willemb@google.com>
---
Changes
v5 -> v6
- do not suppress tx failures on expected failure:
all expect the receiver process to signal failure
v3 -> v4
- replace ethtool with rtnetlink APIs
- expect_fail: correctly handle negative test pfifofast beyond_hw_horizon,
also when KSFT_MACHINE_SLOW suppresses timing errors
- commit-msg: clarify that rcvtimeout increase is also needed for
beyond_hw_horizon test
- define the horizon (50ms) once, rather than three times
- leave cfg.require_ipver in place
v2 -> v3
- remove drivers/net/settings timeout change: superseded by recent commit
- add reverse_order comment
v1 -> v2
- re-raise NlError from e (patchwork pylint)
- simplify expect_pass test (patchwork pylint)
---
.../testing/selftests/drivers/net/so_txtime.c | 4 +-
.../selftests/drivers/net/so_txtime.py | 74 ++++++++++++++++++-
2 files changed, 74 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/so_txtime.c b/tools/testing/selftests/drivers/net/so_txtime.c
index c40e24cdef69..8c81160656fb 100644
--- a/tools/testing/selftests/drivers/net/so_txtime.c
+++ b/tools/testing/selftests/drivers/net/so_txtime.c
@@ -155,7 +155,7 @@ static void do_recv_verify_empty(int fdr)
char rbuf[1];
int ret;
- ret = recv(fdr, rbuf, sizeof(rbuf), 0);
+ ret = recv(fdr, rbuf, sizeof(rbuf), MSG_DONTWAIT);
if (ret != -1 || errno != EAGAIN)
error(1, 0, "recv: not empty as expected (%d, %d)", ret, errno);
}
@@ -379,7 +379,7 @@ static int setup_tx(struct sockaddr *addr, socklen_t alen)
static int setup_rx(struct sockaddr *addr, socklen_t alen)
{
- struct timeval tv = { .tv_usec = 100 * 1000 };
+ struct timeval tv = { .tv_usec = 600 * 1000 };
int fd;
fd = socket(addr->sa_family, SOCK_DGRAM, 0);
diff --git a/tools/testing/selftests/drivers/net/so_txtime.py b/tools/testing/selftests/drivers/net/so_txtime.py
index a097fae0b335..a6ccaf134199 100755
--- a/tools/testing/selftests/drivers/net/so_txtime.py
+++ b/tools/testing/selftests/drivers/net/so_txtime.py
@@ -12,8 +12,9 @@ import time
from lib.py import ksft_exit, ksft_run, ksft_variants
from lib.py import KsftNamedVariant, KsftSkipEx
from lib.py import NetDrvEpEnv, bkg, cmd, defer, tc
-from lib.py import CmdExitFailure
+from lib.py import CmdExitFailure, RtnlFamily, NlError
+_HW_OFFLOAD_HORIZON_MS = 50
def test_so_txtime(cfg, clockid, ipver, args_tx, args_rx, expect_success):
"""Main function. Run so_txtime as sender and receiver."""
@@ -33,12 +34,41 @@ def test_so_txtime(cfg, clockid, ipver, args_tx, args_rx, expect_success):
expect_fail = not expect_success
if slow_machine:
expect_success = False
+ expect_fail = None
with bkg(cmd_rx, host=cfg.remote, fail=expect_success,
expect_fail=expect_fail, exit_wait=True):
cmd(cmd_tx)
+def _dev_setup_pacing_offload(cfg):
+ """Configure pacing-offload-horizon."""
+ rtnl = RtnlFamily()
+
+ try:
+ link = rtnl.getlink({'ifi-index': cfg.ifindex})
+ except NlError as e:
+ raise KsftSkipEx('getlink not supported by device') from e
+
+ if 'pacing-offload-horizon' not in link or \
+ 'max-pacing-offload-horizon' not in link:
+ raise KsftSkipEx('pacing offload horizon not supported by device')
+
+ horizon = _HW_OFFLOAD_HORIZON_MS * 1000_000
+ if link['max-pacing-offload-horizon'] < horizon:
+ raise KsftSkipEx('pacing offload max horizon too small')
+
+ cur_horizon = link['pacing-offload-horizon']
+ rtnl.setlink({
+ 'ifi-index': cfg.ifindex,
+ 'pacing-offload-horizon': horizon,
+ })
+ defer(rtnl.setlink, {
+ 'ifi-index': cfg.ifindex,
+ 'pacing-offload-horizon': cur_horizon
+ })
+
+
def _qdisc_setup(ifname, qdisc, optargs=""):
"""Replace root qdisc. Restore the original after the test.
@@ -61,6 +91,7 @@ def _test_variants_fq():
["one_pkt", "a,10", "a,10"],
["in_order", "a,10,b,20", "a,10,b,20"],
["reverse_order", "a,20,b,10", "b,10,a,20"],
+ ["beyond_hw_horizon", "a,70", "a,70"],
]:
name = f"v{ipver}_{testcase[0]}"
yield KsftNamedVariant(name, ipver, testcase[1], testcase[2])
@@ -74,6 +105,39 @@ def test_so_txtime_fq_mono(cfg, ipver, args_tx, args_rx):
test_so_txtime(cfg, "mono", ipver, args_tx, args_rx, True)
+@ksft_variants(_test_variants_fq())
+def test_so_txtime_fq_mono_hw(cfg, ipver, args_tx, args_rx):
+ """Run all variants of monotonic fq tests, with offload horizon."""
+ cfg.require_ipver(ipver)
+ cfg.require_nsim(nsim_test=False)
+
+ _dev_setup_pacing_offload(cfg)
+ try:
+ _qdisc_setup(cfg.ifname, "fq", f"offload_horizon {_HW_OFFLOAD_HORIZON_MS}ms")
+ except Exception as e:
+ raise KsftSkipEx("netdev does not support offload. skipping") from e
+
+ # Expect all tests to use only hw pacing, except beyond_hw_horizon.
+ # Do not pass -H to that test so that with sw pacing fallback it passes.
+ hw_only = "-H" if args_tx != "a,70" else ""
+ test_so_txtime(cfg, "mono", ipver, f"{hw_only} {args_tx}", args_rx, True)
+
+
+@ksft_variants(_test_variants_fq())
+def test_so_txtime_pfifofast_mono_hw(cfg, ipver, args_tx, args_rx):
+ """Run all variants of monotonic tests, without fq pacing sw backup."""
+ cfg.require_ipver(ipver)
+ cfg.require_nsim(nsim_test=False)
+
+ _dev_setup_pacing_offload(cfg)
+ _qdisc_setup(cfg.ifname, "pfifo_fast")
+
+ # Expect all tests to pass, except beyond_hw_horizon without sw fallback.
+ # It will send immediately, failing the receiver arrival bounds check.
+ expect_pass = not args_tx == "a,70"
+ test_so_txtime(cfg, "mono", ipver, f"-H {args_tx}", args_rx, expect_pass)
+
+
@ksft_variants(_test_variants_fq())
def test_so_txtime_fq_tai(cfg, ipver, args_tx, args_rx):
"""Run all variants of fq tests, but pass CLOCK_TAI to test conversion."""
@@ -123,7 +187,13 @@ def main() -> None:
"""Boilerplate ksft main."""
with NetDrvEpEnv(__file__) as cfg:
ksft_run(
- [test_so_txtime_fq_mono, test_so_txtime_fq_tai, test_so_txtime_etf],
+ [
+ test_so_txtime_fq_mono,
+ test_so_txtime_fq_mono_hw,
+ test_so_txtime_pfifofast_mono_hw,
+ test_so_txtime_fq_tai,
+ test_so_txtime_etf,
+ ],
args=(cfg,),
)
ksft_exit()
--
2.55.0.691.gc56d675ccc-goog
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH net-next v6 2/6] net_sched: sch_fq: clear past skb->tstamp if offloading pacing
2026-08-13 2:03 ` [PATCH net-next v6 2/6] net_sched: sch_fq: clear past skb->tstamp if offloading pacing Willem de Bruijn
@ 2026-08-13 5:12 ` Eric Dumazet
0 siblings, 0 replies; 8+ messages in thread
From: Eric Dumazet @ 2026-08-13 5:12 UTC (permalink / raw)
To: Willem de Bruijn
Cc: netdev, davem, kuba, pabeni, horms, andrew+netdev,
Willem de Bruijn
On Thu, Aug 13, 2026 at 4:05 AM Willem de Bruijn
<willemdebruijn.kernel@gmail.com> wrote:
>
> From: Willem de Bruijn <willemb@google.com>
>
> When hardware offload is enabled, FQ will forward packets to the
> netdevice for pacing. The device has to test that skb->tstamp is
> in the future.
>
> Avoid this cost for packets whose txtime has already passed, by
> clearing skb->tstamp.
>
> Also clear slightly into the future, for EDT timestamps that are
> so close to now that they fall within a reasonable normal Tx
> latency. This slack is set to 400 nsec.
>
> Also disable timer drift logic when offload is enabled, because
> time_next_packet can exceed now causing a negative value.
>
> Signed-off-by: Willem de Bruijn <willemb@google.com>
>
> ---
> @@ -828,11 +837,16 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
> * f->time_next_packet was set when prior packet was sent,
> * and current time (@now) can be too late by tens of us.
> */
> - if (f->time_next_packet)
> + if (f->time_next_packet && f->time_next_packet < now)
> len -= min(len/2, now - f->time_next_packet);
IMO this part deserves a patch on its own.
Using a variable adds no cost (compiler will generate the same code),
but helps to understand what is going on?
commit e51f3936aba0e5887cf5884b18ffa769dcb38523
Author: Eric Dumazet <edumazet@google.com>
Date: Wed Jul 1 08:33:38 2026 +0000
net_sched: sch_fq: fix pacing delay underflow with pacing offload
When pacing offload is enabled (q->offload_horizon > 0),
FQ can dequeue packets early (now < f->time_next_packet).
In this case, the drift calculation (now - f->time_next_packet)
underflows to a large unsigned value.
min(len/2, now - f->time_next_packet) then evaluates to len/2,
incorrectly halving the pacing delay for the next packet.
Fix this by only applying drift compensation when now > f->time_next_packet.
This bug was triggered when flow_max_rate was set on the qdisc
or for non EDT packets (packets with a zero skb->tstamp).
Fixes: f26080d47007 ("net_sched: sch_fq: add the ability to offload pacing")
Reported-by: Willem de Bruijn <willemb@google.com>
Signed-off-by: Eric Dumazet <edumazet@google.com>
diff --git a/net/sched/sch_fq.c b/net/sched/sch_fq.c
index 7cae082a984721215b17fea2d094095155580dcb..f2a65a7a928d5838547285921ce0b42c99a96ef4
100644
--- a/net/sched/sch_fq.c
+++ b/net/sched/sch_fq.c
@@ -828,8 +828,12 @@ static struct sk_buff *fq_dequeue(struct Qdisc *sch)
* f->time_next_packet was set when prior packet was sent,
* and current time (@now) can be too late by tens of us.
*/
- if (f->time_next_packet)
- len -= min(len/2, now - f->time_next_packet);
+ if (f->time_next_packet) {
+ s64 drift = now - f->time_next_packet;
+
+ if (drift > 0)
+ len -= min_t(u64, len/2, drift);
+ }
f->time_next_packet = now + len;
}
out:
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-08-13 5:13 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-13 2:03 [PATCH net-next v6 0/6] hardware pacing offload Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 1/6] net: rtnetlink: add pacing_offload_horizon attribute to net_device Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 2/6] net_sched: sch_fq: clear past skb->tstamp if offloading pacing Willem de Bruijn
2026-08-13 5:12 ` Eric Dumazet
2026-08-13 2:03 ` [PATCH net-next v6 3/6] idpf: support pacing offload Willem de Bruijn
2026-08-13 2:03 ` [PATCH net-next v6 4/6] selftests: drv-net: refactor so_txtime errqueue handling Willem de Bruijn
2026-08-13 2:04 ` [PATCH net-next v6 5/6] selftests: drv-net: in so_txtime tell apart sw from hw pacing Willem de Bruijn
2026-08-13 2:04 ` [PATCH net-next v6 6/6] selftests: drv-net: extend so_txtime with hw offload Willem de Bruijn
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox