All of lore.kernel.org
 help / color / mirror / Atom feed
From: Stanislaw Pal <kuncy7@gmail.com>
To: Christian Marangi <ansuelsmth@gmail.com>,
	Andrew Lunn <andrew@lunn.ch>, Vladimir Oltean <olteanv@gmail.com>
Cc: "David S . Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@kernel.org>,
	Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH net-next] net: dsa: qca8k: report port statistics from the MIB
Date: Sun, 18 Oct 2026 17:36:58 +0200	[thread overview]
Message-ID: <20261018153658.209999-1-kuncy7@gmail.com> (raw)

qca8k has no .get_stats64, so a user port's netdev counts only the
frames that cross the CPU port, through the DSA user netdev's own
tstats. Whatever the switch forwards between its own ports in a
hardware-offloaded bridge never reaches the CPU and never shows up in
the counters of either port. The same goes for a conduit that forwards
in hardware past the host: on a GL.iNet GL-B3000 (IPQ5018 with a
QCA8337 on SGMII) with the conduit offloaded, a 482 MB upload over the
WAN port moved the wan netdev's TX bytes by 0.74 MB, while the switch's
own TxByte counter for that port grew by 503 MB.

The switch counts every frame in its per-port MIB. .get_stats64 may be
called in atomic context and the MIB sits behind MDIO (or the Ethernet
management frames), so read it from a delayed work every 3 s, as ar9331
and rtl8365mb do, and hand out the cached result. The counters keep
their value across reads (QCA8K_MIB_CPU_KEEP); only the difference since
the previous read is added, so a 32-bit counter wrapping between two
reads does not corrupt the total.

With the change, on the same board and setup, a 732 MB upload moved the
wan netdev's TX bytes by 764.9 MB, the same as the switch's TxByte.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Stanislaw Pal <kuncy7@gmail.com>
---
Tested on a GL.iNet GL-B3000 with the patch on OpenWrt's 6.18 kernel,
where it differs only in the context of struct qca8k_priv. Built for
arm64 on net-next with W=1; checkpatch --strict is clean.

 drivers/net/dsa/qca/qca8k-8xxx.c | 157 +++++++++++++++++++++++++++++++
 drivers/net/dsa/qca/qca8k.h      |  47 +++++++++
 2 files changed, 204 insertions(+)

diff --git a/drivers/net/dsa/qca/qca8k-8xxx.c b/drivers/net/dsa/qca/qca8k-8xxx.c
index 07227ae8b..9fef54a88 100644
--- a/drivers/net/dsa/qca/qca8k-8xxx.c
+++ b/drivers/net/dsa/qca/qca8k-8xxx.c
@@ -1851,6 +1851,156 @@ static void qca8k_setup_hol_fixup(struct qca8k_priv *priv, int port)
 			   mask);
 }
 
+/* Without .get_stats64 a DSA user port counts only what crosses the CPU
+ * port, which is nothing of the traffic the switch forwards between its own
+ * ports. Report the switch's own port counters instead.
+ */
+#define QCA8K_STATS_INTERVAL	(3 * HZ)
+
+static const u8 qca8k_stats_mib[QCA8K_STATS_MIB_COUNT] = {
+	[QCA8K_STATS_RX_MULTI]		= 0x08,
+	[QCA8K_STATS_RX_FCS_ERR]	= 0x0c,
+	[QCA8K_STATS_RX_ALIGN_ERR]	= 0x10,
+	[QCA8K_STATS_RX_RUNT]		= 0x14,
+	[QCA8K_STATS_RX_FRAGMENT]	= 0x18,
+	[QCA8K_STATS_RX_64]		= 0x1c,
+	[QCA8K_STATS_RX_128]		= 0x20,
+	[QCA8K_STATS_RX_256]		= 0x24,
+	[QCA8K_STATS_RX_512]		= 0x28,
+	[QCA8K_STATS_RX_1024]		= 0x2c,
+	[QCA8K_STATS_RX_1518]		= 0x30,
+	[QCA8K_STATS_RX_MAX]		= 0x34,
+	[QCA8K_STATS_RX_TOO_LONG]	= 0x38,
+	[QCA8K_STATS_RX_OVERFLOW]	= 0x4c,
+	[QCA8K_STATS_FILTERED]		= 0x50,
+	[QCA8K_STATS_TX_UNDERRUN]	= 0x60,
+	[QCA8K_STATS_TX_64]		= 0x64,
+	[QCA8K_STATS_TX_128]		= 0x68,
+	[QCA8K_STATS_TX_256]		= 0x6c,
+	[QCA8K_STATS_TX_512]		= 0x70,
+	[QCA8K_STATS_TX_1024]		= 0x74,
+	[QCA8K_STATS_TX_1518]		= 0x78,
+	[QCA8K_STATS_TX_MAX]		= 0x7c,
+	[QCA8K_STATS_TX_OVERSIZE]	= 0x80,
+	[QCA8K_STATS_TX_COLLISION]	= 0x8c,
+	[QCA8K_STATS_TX_ABORT_COL]	= 0x90,
+	[QCA8K_STATS_TX_LATE_COL]	= 0xa4,
+};
+
+#define QCA8K_MIB_RX_GOOD_BYTE	0x3c
+#define QCA8K_MIB_TX_BYTE	0x84
+
+static int qca8k_stats_read64(struct qca8k_priv *priv, u32 reg, u64 *val)
+{
+	u32 lo, hi;
+	int ret;
+
+	ret = qca8k_read(priv, reg, &lo);
+	if (!ret)
+		ret = qca8k_read(priv, reg + 4, &hi);
+	if (!ret)
+		*val = ((u64)hi << 32) | lo;
+
+	return ret;
+}
+
+static u64 qca8k_stats_delta64(u64 now, u64 *last)
+{
+	/* a flushed MIB starts again from zero */
+	u64 delta = now >= *last ? now - *last : now;
+
+	*last = now;
+	return delta;
+}
+
+static void qca8k_stats_read_port(struct qca8k_priv *priv, int port)
+{
+	struct qca8k_port_stats *ps = &priv->port_stats[port];
+	u32 raw[QCA8K_STATS_MIB_COUNT], d[QCA8K_STATS_MIB_COUNT];
+	u32 base = QCA8K_PORT_MIB_COUNTER(port);
+	struct rtnl_link_stats64 *s = &ps->stats;
+	u64 rx_bytes, tx_bytes;
+	int i;
+
+	/* all of the slow MDIO reads before the lock */
+	if (qca8k_stats_read64(priv, base + QCA8K_MIB_RX_GOOD_BYTE, &rx_bytes) ||
+	    qca8k_stats_read64(priv, base + QCA8K_MIB_TX_BYTE, &tx_bytes))
+		return;
+
+	for (i = 0; i < QCA8K_STATS_MIB_COUNT; i++)
+		if (qca8k_read(priv, base + qca8k_stats_mib[i], &raw[i]))
+			return;
+
+	spin_lock_bh(&ps->lock);
+
+	for (i = 0; i < QCA8K_STATS_MIB_COUNT; i++) {
+		d[i] = raw[i] - ps->last[i];
+		ps->last[i] = raw[i];
+	}
+
+	s->rx_bytes += qca8k_stats_delta64(rx_bytes, &ps->last_rx_bytes);
+	s->tx_bytes += qca8k_stats_delta64(tx_bytes, &ps->last_tx_bytes);
+	s->rx_packets += (u64)d[QCA8K_STATS_RX_64] + d[QCA8K_STATS_RX_128] +
+			 d[QCA8K_STATS_RX_256] + d[QCA8K_STATS_RX_512] +
+			 d[QCA8K_STATS_RX_1024] + d[QCA8K_STATS_RX_1518] +
+			 d[QCA8K_STATS_RX_MAX];
+	s->tx_packets += (u64)d[QCA8K_STATS_TX_64] + d[QCA8K_STATS_TX_128] +
+			 d[QCA8K_STATS_TX_256] + d[QCA8K_STATS_TX_512] +
+			 d[QCA8K_STATS_TX_1024] + d[QCA8K_STATS_TX_1518] +
+			 d[QCA8K_STATS_TX_MAX];
+	s->multicast += d[QCA8K_STATS_RX_MULTI];
+	s->rx_length_errors += (u64)d[QCA8K_STATS_RX_RUNT] +
+			       d[QCA8K_STATS_RX_FRAGMENT] +
+			       d[QCA8K_STATS_RX_TOO_LONG];
+	s->rx_crc_errors += d[QCA8K_STATS_RX_FCS_ERR];
+	s->rx_frame_errors += d[QCA8K_STATS_RX_ALIGN_ERR];
+	s->rx_missed_errors += d[QCA8K_STATS_RX_OVERFLOW];
+	s->rx_dropped += d[QCA8K_STATS_FILTERED];
+	s->rx_errors += (u64)d[QCA8K_STATS_RX_FCS_ERR] +
+			d[QCA8K_STATS_RX_ALIGN_ERR] + d[QCA8K_STATS_RX_RUNT] +
+			d[QCA8K_STATS_RX_FRAGMENT] + d[QCA8K_STATS_RX_OVERFLOW] +
+			d[QCA8K_STATS_RX_TOO_LONG];
+	s->tx_fifo_errors += d[QCA8K_STATS_TX_UNDERRUN];
+	s->tx_aborted_errors += d[QCA8K_STATS_TX_ABORT_COL];
+	s->tx_window_errors += d[QCA8K_STATS_TX_LATE_COL];
+	s->tx_errors += (u64)d[QCA8K_STATS_TX_OVERSIZE] +
+			d[QCA8K_STATS_TX_ABORT_COL] + d[QCA8K_STATS_TX_UNDERRUN] +
+			d[QCA8K_STATS_TX_LATE_COL];
+	s->collisions += d[QCA8K_STATS_TX_COLLISION];
+
+	spin_unlock_bh(&ps->lock);
+}
+
+static void qca8k_stats_poll(struct work_struct *work)
+{
+	struct qca8k_priv *priv = container_of(work, struct qca8k_priv,
+					       stats_work.work);
+	struct dsa_port *dp;
+
+	dsa_switch_for_each_user_port(dp, priv->ds)
+		qca8k_stats_read_port(priv, dp->index);
+
+	schedule_delayed_work(&priv->stats_work, QCA8K_STATS_INTERVAL);
+}
+
+static void qca8k_get_stats64(struct dsa_switch *ds, int port,
+			      struct rtnl_link_stats64 *s)
+{
+	struct qca8k_priv *priv = ds->priv;
+	struct qca8k_port_stats *ps = &priv->port_stats[port];
+
+	spin_lock_bh(&ps->lock);
+	memcpy(s, &ps->stats, sizeof(*s));
+	spin_unlock_bh(&ps->lock);
+}
+
+static void qca8k_teardown(struct dsa_switch *ds)
+{
+	struct qca8k_priv *priv = ds->priv;
+
+	cancel_delayed_work_sync(&priv->stats_work);
+}
+
 static int
 qca8k_setup(struct dsa_switch *ds)
 {
@@ -2020,6 +2170,11 @@ qca8k_setup(struct dsa_switch *ds)
 	/* Set max number of LAGs supported */
 	ds->num_lag_ids = QCA8K_NUM_LAGS;
 
+	for (int port = 0; port < QCA8K_NUM_PORTS; port++)
+		spin_lock_init(&priv->port_stats[port].lock);
+	INIT_DELAYED_WORK(&priv->stats_work, qca8k_stats_poll);
+	schedule_delayed_work(&priv->stats_work, 0);
+
 	return 0;
 }
 
@@ -2033,9 +2188,11 @@ static const struct phylink_mac_ops qca8k_phylink_mac_ops = {
 static const struct dsa_switch_ops qca8k_switch_ops = {
 	.get_tag_protocol	= qca8k_get_tag_protocol,
 	.setup			= qca8k_setup,
+	.teardown		= qca8k_teardown,
 	.get_strings		= qca8k_get_strings,
 	.get_ethtool_stats	= qca8k_get_ethtool_stats,
 	.get_sset_count		= qca8k_get_sset_count,
+	.get_stats64		= qca8k_get_stats64,
 	.set_ageing_time	= qca8k_set_ageing_time,
 	.support_eee		= dsa_supports_eee,
 	.set_mac_eee		= qca8k_set_mac_eee,
diff --git a/drivers/net/dsa/qca/qca8k.h b/drivers/net/dsa/qca/qca8k.h
index 956338893..47e9e5631 100644
--- a/drivers/net/dsa/qca/qca8k.h
+++ b/drivers/net/dsa/qca/qca8k.h
@@ -442,6 +442,51 @@ struct qca8k_led {
 	struct led_classdev cdev;
 };
 
+/* MIB counters behind .get_stats64, in the order qca8k_stats_mib[] reads them */
+enum qca8k_stats_mib_idx {
+	QCA8K_STATS_RX_MULTI,
+	QCA8K_STATS_RX_FCS_ERR,
+	QCA8K_STATS_RX_ALIGN_ERR,
+	QCA8K_STATS_RX_RUNT,
+	QCA8K_STATS_RX_FRAGMENT,
+	QCA8K_STATS_RX_64,
+	QCA8K_STATS_RX_128,
+	QCA8K_STATS_RX_256,
+	QCA8K_STATS_RX_512,
+	QCA8K_STATS_RX_1024,
+	QCA8K_STATS_RX_1518,
+	QCA8K_STATS_RX_MAX,
+	QCA8K_STATS_RX_TOO_LONG,
+	QCA8K_STATS_RX_OVERFLOW,
+	QCA8K_STATS_FILTERED,
+	QCA8K_STATS_TX_UNDERRUN,
+	QCA8K_STATS_TX_64,
+	QCA8K_STATS_TX_128,
+	QCA8K_STATS_TX_256,
+	QCA8K_STATS_TX_512,
+	QCA8K_STATS_TX_1024,
+	QCA8K_STATS_TX_1518,
+	QCA8K_STATS_TX_MAX,
+	QCA8K_STATS_TX_OVERSIZE,
+	QCA8K_STATS_TX_COLLISION,
+	QCA8K_STATS_TX_ABORT_COL,
+	QCA8K_STATS_TX_LATE_COL,
+	QCA8K_STATS_MIB_COUNT
+};
+
+/* Port counters for .get_stats64. That callback runs in atomic context and
+ * the MIB sits behind MDIO, so a worker reads it and this holds the result.
+ * The MIB counters are cumulative; last[] keeps the previous raw values so
+ * only the difference is added and a 32-bit counter may wrap between reads.
+ */
+struct qca8k_port_stats {
+	spinlock_t lock; /* protects stats against the worker */
+	struct rtnl_link_stats64 stats;
+	u32 last[QCA8K_STATS_MIB_COUNT];
+	u64 last_rx_bytes;
+	u64 last_tx_bytes;
+};
+
 struct qca8k_priv {
 	u8 switch_id;
 	u8 switch_revision;
@@ -469,6 +514,8 @@ struct qca8k_priv {
 	struct qca8k_pcs pcs_port_6;
 	const struct qca8k_match_data *info;
 	struct qca8k_led ports_led[QCA8K_LED_COUNT];
+	struct qca8k_port_stats port_stats[QCA8K_NUM_PORTS];
+	struct delayed_work stats_work;
 };
 
 struct qca8k_mib_desc {
-- 
2.43.0


             reply	other threads:[~2026-10-18 15:37 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-18 15:36 Stanislaw Pal [this message]
2026-10-18 21:55 ` [PATCH net-next] net: dsa: qca8k: report port statistics from the MIB Christian Marangi
2026-10-19 10:51   ` Stanislaw Pal
2026-10-19 15:39 ` netdev-bot+sashiko

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261018153658.209999-1-kuncy7@gmail.com \
    --to=kuncy7@gmail.com \
    --cc=andrew@lunn.ch \
    --cc=ansuelsmth@gmail.com \
    --cc=davem@davemloft.net \
    --cc=edumazet@kernel.org \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=olteanv@gmail.com \
    --cc=pabeni@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.