Netdev List
 help / color / mirror / Atom feed
From: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
To: davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
	pabeni@redhat.com, andrew+netdev@lunn.ch, corbet@lwn.net
Cc: horms@kernel.org, skhan@linuxfoundation.org,
	rdunlap@infradead.org, netdev@vger.kernel.org,
	linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org,
	Subash Abhinov Kasiviswanathan
	<subash.a.kasiviswanathan@oss.qualcomm.com>,
	Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Subject: [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics
Date: Wed,  7 Oct 2026 17:55:44 -0700	[thread overview]
Message-ID: <20261008005543.2630828-8-subash.a.kasiviswanathan@oss.qualcomm.com> (raw)
In-Reply-To: <20261008005543.2630828-1-subash.a.kasiviswanathan@oss.qualcomm.com>

Track downlink coalescing activity with standard network stack RX
statistics and rmnet specific ethtool diagnostics.

Expose these standard RX statistics through netdev_stat_ops:

- HW-GRO packets and the number of wire packets represented by them
- RX allocation failures
- RX drops for reconstructed packets discarded by rmnet

Keep these rmnet specific ethtool statistics for diagnostics that have no
standard representation:

- Coalescing header, packet count and bounds errors
- Invalid IP and transport headers
- Segment reconstruction
- Hardware close reasons

Store the per-CPU counters as u64_stats_t values and update them with
u64_stats_inc() and u64_stats_add() under u64_stats_sync protection. Copy
them into plain snapshot structures with u64_stats_copy() before
aggregation. This avoids torn counter reads during reporting.

Checksum error diagnostics distinguish hardware reported errors from
packets actually dropped. IPv4 UDP packets with a zero checksum are
accepted and are not counted as drops.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2:
  - Use per cpu counters to avoid torn reads
  - Add network stack specific stats for HW_GRO
  - Add a build check for ethtool fields
  - Update comments and commit text explaining the stats
v1: https://lore.kernel.org/all/20260930051345.857443-7-subash.a.kasiviswanathan@oss.qualcomm.com/

 .../ethernet/qualcomm/rmnet/rmnet_config.h    |  84 ++++-
 .../ethernet/qualcomm/rmnet/rmnet_map_data.c  | 338 ++++++++++++++++--
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.c   | 124 ++++++-
 3 files changed, 499 insertions(+), 47 deletions(-)

diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
index 5adda0323dda..c71587c843e4 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
@@ -63,12 +63,14 @@ struct rmnet_vnd_stats {
 	u32 tx_drops;
 };
 
-struct rmnet_pcpu_stats {
-	struct rmnet_vnd_stats stats;
-	struct u64_stats_sync syncp;
-};
-
 struct rmnet_priv_stats {
+	/* Network stack RX statistics for all rmnet receive paths */
+	u64 rx_hw_gro_packets;
+	u64 rx_hw_gro_wire_packets;
+	u64 rx_alloc_fail;
+	u64 rx_dropped;
+
+	/* Custom diagnostics for the coalescing receive path */
 	u64 csum_ok;
 	u64 csum_ip4_header_bad;
 	u64 csum_valid_unset;
@@ -80,6 +82,77 @@ struct rmnet_priv_stats {
 	u64 csum_skipped;
 	u64 csum_sw;
 	u64 csum_hw;
+	/* Coalescing-only counters. Some are subsets of the standard counters. */
+	u64 coal_alloc_fail;
+	u64 coal_rx;
+	u64 coal_pkts;
+	u64 coal_hdr_err;
+	u64 coal_hdr_pkt_err;
+	u64 coal_bounds_err;
+	u64 coal_csum_err;
+	u64 coal_csum_drop;
+	u64 coal_reconstruct;
+	u64 coal_ip_invalid;
+	u64 coal_trans_invalid;
+	/* close-reason sub-counters */
+	u64 coal_close_non_coal;
+	u64 coal_close_ip_miss;
+	u64 coal_close_trans_miss;
+	u64 coal_close_hw_nl;
+	u64 coal_close_hw_pkt;
+	u64 coal_close_hw_byte;
+	u64 coal_close_hw_time;
+	u64 coal_close_hw_evict;
+	u64 coal_close_coal;
+};
+
+struct rmnet_pcpu_priv_stats {
+	/* Network stack RX statistics for all rmnet receive paths */
+	u64_stats_t rx_hw_gro_packets;
+	u64_stats_t rx_hw_gro_wire_packets;
+	u64_stats_t rx_alloc_fail;
+	u64_stats_t rx_dropped;
+
+	/* Custom diagnostics for the coalescing receive path */
+	u64_stats_t csum_ok;
+	u64_stats_t csum_ip4_header_bad;
+	u64_stats_t csum_valid_unset;
+	u64_stats_t csum_validation_failed;
+	u64_stats_t csum_err_bad_buffer;
+	u64_stats_t csum_err_invalid_ip_version;
+	u64_stats_t csum_err_invalid_transport;
+	u64_stats_t csum_fragmented_pkt;
+	u64_stats_t csum_skipped;
+	u64_stats_t csum_sw;
+	u64_stats_t csum_hw;
+	/* Coalescing-only counters. Some are subsets of the standard counters. */
+	u64_stats_t coal_alloc_fail;
+	u64_stats_t coal_rx;
+	u64_stats_t coal_pkts;
+	u64_stats_t coal_hdr_err;
+	u64_stats_t coal_hdr_pkt_err;
+	u64_stats_t coal_bounds_err;
+	u64_stats_t coal_csum_err;
+	u64_stats_t coal_csum_drop;
+	u64_stats_t coal_reconstruct;
+	u64_stats_t coal_ip_invalid;
+	u64_stats_t coal_trans_invalid;
+	/* close-reason sub-counters */
+	u64_stats_t coal_close_non_coal;
+	u64_stats_t coal_close_ip_miss;
+	u64_stats_t coal_close_trans_miss;
+	u64_stats_t coal_close_hw_nl;
+	u64_stats_t coal_close_hw_pkt;
+	u64_stats_t coal_close_hw_byte;
+	u64_stats_t coal_close_hw_time;
+	u64_stats_t coal_close_hw_evict;
+	u64_stats_t coal_close_coal;
+};
+
+struct rmnet_pcpu_stats {
+	struct rmnet_vnd_stats stats;
+	struct rmnet_pcpu_priv_stats priv_stats;
+	struct u64_stats_sync syncp;
 };
 
 struct rmnet_priv {
@@ -87,7 +160,6 @@ struct rmnet_priv {
 	struct net_device *real_dev;
 	struct rmnet_pcpu_stats __percpu *pcpu_stats;
 	struct gro_cells gro_cells;
-	struct rmnet_priv_stats stats;
 };
 
 struct rmnet_port *rmnet_get_port_rcu(const struct net_device *real_dev);
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
index e8adb4006717..74d64b7066e2 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
@@ -53,34 +53,45 @@ rmnet_map_ipv4_dl_csum_trailer(struct sk_buff *skb,
 {
 	struct iphdr *ip4h = (struct iphdr *)skb->data;
 	void *txporthdr = skb->data + ip4h->ihl * 4;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	__sum16 *csum_field, pseudo_csum;
 	__sum16 ip_payload_csum;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	/* Computing the checksum over just the IPv4 header--including its
 	 * checksum field--should yield 0.  If it doesn't, the IP header
 	 * is bad, so return an error and let the IP layer drop it.
 	 */
 	if (ip_fast_csum(ip4h, ip4h->ihl)) {
-		priv->stats.csum_ip4_header_bad++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_ip4_header_bad);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
 	}
 
 	/* We don't support checksum offload on IPv4 fragments */
 	if (ip_is_fragment(ip4h)) {
-		priv->stats.csum_fragmented_pkt++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_fragmented_pkt);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EOPNOTSUPP;
 	}
 
 	/* Checksum offload is only supported for UDP and TCP protocols */
 	csum_field = rmnet_map_get_csum_field(ip4h->protocol, txporthdr);
 	if (!csum_field) {
-		priv->stats.csum_err_invalid_transport++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_transport);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EPROTONOSUPPORT;
 	}
 
 	/* RFC 768: UDP checksum is optional for IPv4, and is 0 if unused */
 	if (!*csum_field && ip4h->protocol == IPPROTO_UDP) {
-		priv->stats.csum_skipped++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_skipped);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return 0;
 	}
 
@@ -113,11 +124,15 @@ rmnet_map_ipv4_dl_csum_trailer(struct sk_buff *skb,
 
 	/* The cast is required to ensure only the low 16 bits are examined */
 	if (ip_payload_csum != (__sum16)~pseudo_csum) {
-		priv->stats.csum_validation_failed++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_validation_failed);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
 	}
 
-	priv->stats.csum_ok++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_ok);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 	return 0;
 }
 
@@ -129,16 +144,21 @@ rmnet_map_ipv6_dl_csum_trailer(struct sk_buff *skb,
 {
 	struct ipv6hdr *ip6h = (struct ipv6hdr *)skb->data;
 	void *txporthdr = skb->data + sizeof(*ip6h);
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	__sum16 *csum_field, pseudo_csum;
 	__sum16 ip6_payload_csum;
 	__be16 ip_header_csum;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	/* Checksum offload is only supported for UDP and TCP protocols;
 	 * the packet cannot include any IPv6 extension headers
 	 */
 	csum_field = rmnet_map_get_csum_field(ip6h->nexthdr, txporthdr);
 	if (!csum_field) {
-		priv->stats.csum_err_invalid_transport++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_transport);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EPROTONOSUPPORT;
 	}
 
@@ -164,11 +184,15 @@ rmnet_map_ipv6_dl_csum_trailer(struct sk_buff *skb,
 	 * examined.
 	 */
 	if (ip6_payload_csum != (__sum16)~pseudo_csum) {
-		priv->stats.csum_validation_failed++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_validation_failed);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
 	}
 
-	priv->stats.csum_ok++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_ok);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 	return 0;
 }
 #else
@@ -263,6 +287,9 @@ static void rmnet_map_v5_checksum_uplink_packet(struct sk_buff *skb,
 {
 	struct rmnet_priv *priv = netdev_priv(orig_dev);
 	struct rmnet_map_v5_csum_header *ul_header;
+	struct rmnet_pcpu_stats *pcpu_ptr;
+
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
 
 	ul_header = skb_push(skb, sizeof(*ul_header));
 	memset(ul_header, 0, sizeof(*ul_header));
@@ -287,7 +314,9 @@ static void rmnet_map_v5_checksum_uplink_packet(struct sk_buff *skb,
 			proto = ((struct ipv6hdr *)iph)->nexthdr;
 			trans = iph + ip_len;
 		} else {
-			priv->stats.csum_err_invalid_ip_version++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_ip_version);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			goto sw_csum;
 		}
 
@@ -296,13 +325,17 @@ static void rmnet_map_v5_checksum_uplink_packet(struct sk_buff *skb,
 			skb->ip_summed = CHECKSUM_NONE;
 			/* Ask for checksum offloading */
 			ul_header->csum_info |= MAPV5_CSUMINFO_VALID_FLAG;
-			priv->stats.csum_hw++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_hw);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return;
 		}
 	}
 
 sw_csum:
-	priv->stats.csum_sw++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_sw);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 }
 
 /* Adds MAP header to front of skb->data
@@ -434,16 +467,23 @@ int rmnet_map_checksum_downlink_packet(struct sk_buff *skb, u16 len)
 {
 	struct rmnet_priv *priv = netdev_priv(skb->dev);
 	struct rmnet_map_dl_csum_trailer *csum_trailer;
+	struct rmnet_pcpu_stats *pcpu_ptr;
+
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
 
 	if (unlikely(!(skb->dev->features & NETIF_F_RXCSUM))) {
-		priv->stats.csum_sw++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_sw);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EOPNOTSUPP;
 	}
 
 	csum_trailer = (struct rmnet_map_dl_csum_trailer *)(skb->data + len);
 
 	if (!(csum_trailer->flags & MAP_CSUM_DL_VALID_FLAG)) {
-		priv->stats.csum_valid_unset++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_valid_unset);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
 	}
 
@@ -453,7 +493,9 @@ int rmnet_map_checksum_downlink_packet(struct sk_buff *skb, u16 len)
 	if (IS_ENABLED(CONFIG_IPV6) && skb->protocol == htons(ETH_P_IPV6))
 		return rmnet_map_ipv6_dl_csum_trailer(skb, csum_trailer, priv);
 
-	priv->stats.csum_err_invalid_ip_version++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_ip_version);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 
 	return -EPROTONOSUPPORT;
 }
@@ -463,8 +505,11 @@ static void rmnet_map_v4_checksum_uplink_packet(struct sk_buff *skb,
 {
 	struct rmnet_priv *priv = netdev_priv(orig_dev);
 	struct rmnet_map_ul_csum_header *ul_header;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	void *iphdr;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	ul_header = (struct rmnet_map_ul_csum_header *)
 		    skb_push(skb, sizeof(struct rmnet_map_ul_csum_header));
 
@@ -480,22 +525,30 @@ static void rmnet_map_v4_checksum_uplink_packet(struct sk_buff *skb,
 
 	if (skb->protocol == htons(ETH_P_IP)) {
 		rmnet_map_ipv4_ul_csum_header(iphdr, ul_header, skb);
-		priv->stats.csum_hw++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_hw);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return;
 	}
 
 	if (IS_ENABLED(CONFIG_IPV6) && skb->protocol == htons(ETH_P_IPV6)) {
 		rmnet_map_ipv6_ul_csum_header(iphdr, ul_header, skb);
-		priv->stats.csum_hw++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_hw);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return;
 	}
 
-	priv->stats.csum_err_invalid_ip_version++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_ip_version);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 
 sw_csum:
 	memset(ul_header, 0, sizeof(*ul_header));
 
-	priv->stats.csum_sw++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_sw);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 }
 
 /* Generates UL checksum meta info header for IPv4 and IPv6 over TCP and UDP
@@ -665,9 +718,13 @@ __rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 			     bool csum_valid)
 {
 	u32 dlen = coal_meta->data_len * coal_meta->pkt_count;
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
 	u32 hlen = coal_meta->ip_len + coal_meta->trans_len;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	struct sk_buff *skbn;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	/* RFC 768: UDP checksum is optional for IPv4, and is 0 if unused.
 	 * Such packets are never actually bad, regardless of what the
 	 * checksum bitmap says.
@@ -675,12 +732,29 @@ __rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	if (!csum_valid && coal_meta->zero_csum)
 		csum_valid = true;
 
-	if (!csum_valid)
+	if (!csum_valid) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		/* rx_dropped is standard accounting. coal_csum_drop is its
+		 * coalescing-specific checksum error subset.
+		 */
+		u64_stats_add(&pcpu_ptr->priv_stats.rx_dropped, coal_meta->pkt_count);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_csum_drop);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		goto next_pkt;
+	}
 
 	skbn = alloc_skb(hlen + dlen + RMNET_MAP_DEAGGR_HEADROOM, GFP_ATOMIC);
-	if (!skbn)
+	if (!skbn) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		/* The coal counter is the coalescing subset of the standard
+		 * allocation failure and RX drop counters.
+		 */
+		u64_stats_add(&pcpu_ptr->priv_stats.rx_alloc_fail, coal_meta->pkt_count);
+		u64_stats_add(&pcpu_ptr->priv_stats.rx_dropped, coal_meta->pkt_count);
+		u64_stats_add(&pcpu_ptr->priv_stats.coal_alloc_fail, coal_meta->pkt_count);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		goto next_pkt;
+	}
 
 	skb_reserve(skbn, hlen + RMNET_MAP_DEAGGR_HEADROOM);
 	skb_put_data(skbn,
@@ -729,6 +803,9 @@ __rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	rmnet_map_partial_csum(skbn, coal_meta);
 
 	skbn->dev = coal_skb->dev;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.coal_reconstruct);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 
 	if (coal_meta->pkt_count > 1)
 		rmnet_map_gso_stamp(skbn, coal_meta);
@@ -747,14 +824,22 @@ static bool rmnet_map_coal_parse_ip_hdr(struct sk_buff *coal_skb,
 					struct rmnet_map_coal_metadata *meta,
 					bool *gro)
 {
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	struct ipv6hdr *ip6h;
 	struct iphdr *iph;
 	__be16 frag_off;
 	u8 protocol;
 	int ret;
 
-	if (coal_skb->len < sizeof(*iph))
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
+	if (coal_skb->len < sizeof(*iph)) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return false;
+	}
 
 	iph = (struct iphdr *)coal_skb->data;
 
@@ -763,35 +848,58 @@ static bool rmnet_map_coal_parse_ip_hdr(struct sk_buff *coal_skb,
 		meta->ip_len = iph->ihl * 4;
 		meta->trans_proto = iph->protocol;
 		meta->ip_header = iph;
-		if (meta->ip_len < sizeof(*iph) || coal_skb->len < meta->ip_len)
+		if (meta->ip_len < sizeof(*iph) || coal_skb->len < meta->ip_len) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
-		if (ip_is_fragment(iph))
+		if (ip_is_fragment(iph)) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		if (iph->ihl != 5)
 			*gro = false;
 	} else if (iph->version == 6) {
-		if (coal_skb->len < sizeof(*ip6h))
+		if (coal_skb->len < sizeof(*ip6h)) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		ip6h = (struct ipv6hdr *)iph;
 		protocol = ip6h->nexthdr;
 		meta->ip_proto = 6;
 		ret = ipv6_skip_exthdr(coal_skb, sizeof(*ip6h), &protocol,
 				       &frag_off);
-		if (ret < 0 || frag_off || ret > U16_MAX)
+		if (ret < 0 || frag_off || ret > U16_MAX) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		meta->ip_len = (u16)ret;
 		meta->trans_proto = protocol;
 		meta->ip_header = ip6h;
-		if (coal_skb->len < meta->ip_len)
+		if (coal_skb->len < meta->ip_len) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		if (meta->ip_len > sizeof(*ip6h))
 			*gro = false;
 	} else {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return false;
 	}
 
@@ -804,29 +912,49 @@ static bool rmnet_map_coal_parse_ip_hdr(struct sk_buff *coal_skb,
 static bool rmnet_map_coal_parse_trans_hdr(struct sk_buff *coal_skb,
 					   struct rmnet_map_coal_metadata *meta)
 {
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	struct udphdr *uh;
 	struct tcphdr *th;
 	u32 avail;
 	u8 *base;
 
-	if (meta->ip_len > coal_skb->len)
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
+	if (meta->ip_len > coal_skb->len) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return false;
+	}
 
 	base = (u8 *)meta->ip_header + meta->ip_len;
 	avail = coal_skb->len - meta->ip_len;
 
 	if (meta->trans_proto == IPPROTO_TCP) {
-		if (avail < sizeof(*th))
+		if (avail < sizeof(*th)) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		th = (struct tcphdr *)base;
 		meta->trans_len = th->doff * 4;
 		meta->trans_header = th;
-		if (meta->trans_len < sizeof(*th) || avail < meta->trans_len)
+		if (meta->trans_len < sizeof(*th) || avail < meta->trans_len) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 	} else if (meta->trans_proto == IPPROTO_UDP) {
-		if (avail < sizeof(*uh))
+		if (avail < sizeof(*uh)) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		uh = (struct udphdr *)base;
 		meta->trans_len = sizeof(*uh);
@@ -834,6 +962,9 @@ static bool rmnet_map_coal_parse_trans_hdr(struct sk_buff *coal_skb,
 		if (meta->ip_proto == 4 && !uh->check)
 			meta->zero_csum = true;
 	} else {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return false;
 	}
 
@@ -919,12 +1050,16 @@ static void rmnet_map_coal_segment_loop(struct sk_buff *coal_skb,
 					struct sk_buff_head *list,
 					u64 nlo_err_mask, bool gro, u8 num_nlos)
 {
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
 	u32 hlen = coal_meta->ip_len + coal_meta->trans_len;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	u8 pkt, total_pkt = 0;
 	bool csum_err;
 	u16 pkt_len;
 	u8 nlo;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	for (nlo = 0; nlo < num_nlos; nlo++) {
 		pkt_len = ntohs(coal_hdr->nl_pairs[nlo].pkt_len);
 		pkt_len -= hlen;
@@ -939,6 +1074,12 @@ static void rmnet_map_coal_segment_loop(struct sk_buff *coal_skb,
 		     pkt++, total_pkt++, nlo_err_mask >>= 1) {
 			csum_err = nlo_err_mask & 1;
 
+			if (csum_err) {
+				u64_stats_update_begin(&pcpu_ptr->syncp);
+				u64_stats_inc(&pcpu_ptr->priv_stats.coal_csum_err);
+				u64_stats_update_end(&pcpu_ptr->syncp);
+			}
+
 			if (!gro) {
 				coal_meta->pkt_count = 1;
 				__rmnet_map_segment_coal_skb(coal_skb, coal_meta,
@@ -982,11 +1123,15 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 {
 	bool gro_hw = coal_skb->dev->features & NETIF_F_GRO_HW;
 	bool rxcsum = coal_skb->dev->features & NETIF_F_RXCSUM;
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
 	struct rmnet_map_v5_coal_header *coal_hdr;
 	struct rmnet_map_coal_metadata coal_meta;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	bool gro = gro_hw;
 	u32 hlen;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	memset(&coal_meta, 0, sizeof(coal_meta));
 
 	/* Drop any MAP frame padding. The coal header is counted in len */
@@ -1005,8 +1150,12 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	hlen = coal_meta.ip_len + coal_meta.trans_len;
 
 	if (!rmnet_map_coal_validate_bounds(coal_skb, coal_hdr, num_nlos, hlen,
-					    &gro))
+					    &gro)) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_bounds_err);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
+	}
 
 	/* Device capability gates coalesced delivery. Packet format can still
 	 * disable GSO and use the per-packet fallback below.
@@ -1014,6 +1163,16 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	if (total_pkts > 1 && (!rxcsum || !gro_hw))
 		return -EINVAL;
 
+	if (gro && total_pkts > 1) {
+		/* Standard counters cover delivered HW-GRO skbs. coal_rx and
+		 * coal_pkts cover coalescing input regardless of HW-GRO.
+		 */
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.rx_hw_gro_packets);
+		u64_stats_add(&pcpu_ptr->priv_stats.rx_hw_gro_wire_packets, total_pkts);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+	}
+
 	if (total_pkts == 1 && (!rxcsum || !gro_hw)) {
 		coal_skb->ip_summed = CHECKSUM_NONE;
 		__skb_queue_tail(list, coal_skb);
@@ -1036,6 +1195,67 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	return 0;
 }
 
+/* Log the hardware close-reason counter for a coalescing header. */
+static void rmnet_map_data_log_close_stats(struct rmnet_pcpu_stats *pcpu_ptr,
+					   u8 type, u8 code)
+{
+	switch (type) {
+	case RMNET_MAP_COAL_CLOSE_NON_COAL:
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_non_coal);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+		break;
+	case RMNET_MAP_COAL_CLOSE_IP_MISS:
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_ip_miss);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+		break;
+	case RMNET_MAP_COAL_CLOSE_TRANS_MISS:
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_trans_miss);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+		break;
+	case RMNET_MAP_COAL_CLOSE_HW:
+		switch (code) {
+		case RMNET_MAP_COAL_CLOSE_HW_NL:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_nl);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		case RMNET_MAP_COAL_CLOSE_HW_PKT:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_pkt);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		case RMNET_MAP_COAL_CLOSE_HW_BYTE:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_byte);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		case RMNET_MAP_COAL_CLOSE_HW_TIME:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_time);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		case RMNET_MAP_COAL_CLOSE_HW_EVICT:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_evict);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		default:
+			break;
+		}
+		break;
+	case RMNET_MAP_COAL_CLOSE_COAL:
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_coal);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+		break;
+	default:
+		break;
+	}
+}
+
 /* Validate the coalescing header and build the checksum error mask.
  *
  * Checks performed:
@@ -1062,23 +1282,34 @@ static int rmnet_map_data_check_coal_header(struct sk_buff *skb,
 					    u16 *num_pkts)
 {
 	struct rmnet_map_header *maph = (struct rmnet_map_header *)skb->data;
+	struct rmnet_priv *priv = netdev_priv(skb->dev);
 	struct rmnet_map_v5_coal_header *coal_hdr;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	u16 pkts = 0;
 	u64 mask = 0;
 	int nlos;
 	int i;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	/* coal header is counted in pkt_len */
-	if (ntohs(maph->pkt_len) < sizeof(*coal_hdr))
+	if (ntohs(maph->pkt_len) < sizeof(*coal_hdr)) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_hdr_err);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
+	}
 
 	coal_hdr = (struct rmnet_map_v5_coal_header *)(skb->data + sizeof(*maph));
 	nlos = rmnet_map_v5_get_num_nlos(coal_hdr);
-	if (nlos < 0)
+	if (nlos < 0) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_hdr_err);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
+	}
 
 	*num_nlos = nlos;
-
 	for (i = 0; i < RMNET_MAP_V5_MAX_NLOS; i++) {
 		u8 err = coal_hdr->nl_pairs[i].csum_error_bitmap;
 		u8 pkt = coal_hdr->nl_pairs[i].num_packets;
@@ -1086,11 +1317,27 @@ static int rmnet_map_data_check_coal_header(struct sk_buff *skb,
 		mask |= ((u64)err) << (8 * i);
 		if (i < *num_nlos) {
 			pkts += pkt;
-			if (pkts > RMNET_MAP_V5_MAX_PACKETS)
+			if (pkts > RMNET_MAP_V5_MAX_PACKETS) {
+				u64_stats_update_begin(&pcpu_ptr->syncp);
+				u64_stats_inc(&pcpu_ptr->priv_stats.coal_hdr_pkt_err);
+				u64_stats_update_end(&pcpu_ptr->syncp);
 				return -EINVAL;
+			}
 		}
 	}
 
+	/* coal_pkts counts packets reported by the hardware, independent of
+	 * whether the frame is later delivered through HW-GRO.
+	 */
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_add(&pcpu_ptr->priv_stats.coal_pkts, pkts);
+	u64_stats_update_end(&pcpu_ptr->syncp);
+	rmnet_map_data_log_close_stats(pcpu_ptr,
+				       u8_get_bits(coal_hdr->close_info,
+						   MAPV5_CLOSEINFO_CLOSE_TYPE_FMASK),
+				       u8_get_bits(coal_hdr->close_info,
+						   MAPV5_CLOSEINFO_CLOSE_VALUE_FMASK));
+
 	*nlo_err_mask = mask;
 	*num_pkts = pkts;
 	return 0;
@@ -1101,16 +1348,23 @@ int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 				      u16 len, u32 data_format)
 {
 	struct rmnet_priv *priv = netdev_priv(skb->dev);
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	u64 nlo_err_mask;
 	u16 num_pkts;
 	u8 num_nlos;
 	int rc;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	switch (rmnet_map_get_next_hdr_type(skb)) {
 	case RMNET_MAP_HEADER_TYPE_COALESCING:
 		if (!(data_format & RMNET_FLAGS_INGRESS_COALESCE))
 			return -EINVAL;
 
+		/* coal_rx counts coalescing input, not delivered HW-GRO skbs. */
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_rx);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		rc = rmnet_map_data_check_coal_header(skb, &nlo_err_mask,
 						      &num_nlos, &num_pkts);
 		if (rc)
@@ -1135,12 +1389,18 @@ int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 
 	case RMNET_MAP_HEADER_TYPE_CSUM_OFFLOAD:
 		if (unlikely(!(skb->dev->features & NETIF_F_RXCSUM))) {
-			priv->stats.csum_sw++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_sw);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 		} else if (rmnet_map_get_csum_valid(skb)) {
-			priv->stats.csum_ok++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_ok);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			skb->ip_summed = CHECKSUM_UNNECESSARY;
 		} else {
-			priv->stats.csum_valid_unset++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_valid_unset);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 		}
 
 		skb_pull(skb, sizeof(struct rmnet_map_header) +
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
index b8542d2f03b2..8e2a83ca0235 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
@@ -7,6 +7,7 @@
 #include <linux/capability.h>
 #include <linux/etherdevice.h>
 #include <linux/ethtool.h>
+#include <net/netdev_queues.h>
 #include <linux/if_arp.h>
 #include <linux/netlink.h>
 #include <net/pkt_sched.h>
@@ -141,7 +142,9 @@ static void rmnet_vnd_uninit(struct net_device *dev)
 static void rmnet_get_stats64(struct net_device *dev,
 			      struct rtnl_link_stats64 *s)
 {
+	struct rmnet_priv_stats total_priv_stats = { };
 	struct rmnet_priv *priv = netdev_priv(dev);
+	struct rmnet_priv_stats priv_snapshot;
 	struct rmnet_vnd_stats total_stats = { };
 	struct rmnet_pcpu_stats *pcpu_ptr;
 	struct rmnet_vnd_stats snapshot;
@@ -153,6 +156,8 @@ static void rmnet_get_stats64(struct net_device *dev,
 		do {
 			start = u64_stats_fetch_begin(&pcpu_ptr->syncp);
 			snapshot = pcpu_ptr->stats;	/* struct assignment */
+			u64_stats_copy(&priv_snapshot, &pcpu_ptr->priv_stats,
+				       sizeof(priv_snapshot));
 		} while (u64_stats_fetch_retry(&pcpu_ptr->syncp, start));
 
 		total_stats.rx_pkts += snapshot.rx_pkts;
@@ -160,6 +165,7 @@ static void rmnet_get_stats64(struct net_device *dev,
 		total_stats.tx_pkts += snapshot.tx_pkts;
 		total_stats.tx_bytes += snapshot.tx_bytes;
 		total_stats.tx_drops += snapshot.tx_drops;
+		total_priv_stats.rx_dropped += priv_snapshot.rx_dropped;
 	}
 
 	s->rx_packets = total_stats.rx_pkts;
@@ -167,8 +173,45 @@ static void rmnet_get_stats64(struct net_device *dev,
 	s->tx_packets = total_stats.tx_pkts;
 	s->tx_bytes = total_stats.tx_bytes;
 	s->tx_dropped = total_stats.tx_drops;
+	s->rx_dropped = total_priv_stats.rx_dropped;
 }
 
+/* Report standard network stack RX statistics. These are distinct from the
+ * rmnet-specific coalescing diagnostics exposed through ethtool.
+ */
+static void rmnet_get_queue_stats_rx(struct net_device *dev, int idx,
+				     struct netdev_queue_stats_rx *stats)
+{
+	struct rmnet_priv *priv = netdev_priv(dev);
+	struct rmnet_priv_stats total_stats = { };
+	struct rmnet_pcpu_stats *pcpu_ptr;
+	struct rmnet_priv_stats snapshot;
+	unsigned int cpu, start;
+
+	for_each_possible_cpu(cpu) {
+		pcpu_ptr = per_cpu_ptr(priv->pcpu_stats, cpu);
+
+		do {
+			start = u64_stats_fetch_begin(&pcpu_ptr->syncp);
+			u64_stats_copy(&snapshot, &pcpu_ptr->priv_stats,
+				       sizeof(snapshot));
+		} while (u64_stats_fetch_retry(&pcpu_ptr->syncp, start));
+
+		total_stats.rx_hw_gro_packets += snapshot.rx_hw_gro_packets;
+		total_stats.rx_hw_gro_wire_packets +=
+			snapshot.rx_hw_gro_wire_packets;
+		total_stats.rx_alloc_fail += snapshot.rx_alloc_fail;
+	}
+
+	stats->hw_gro_packets = total_stats.rx_hw_gro_packets;
+	stats->hw_gro_wire_packets = total_stats.rx_hw_gro_wire_packets;
+	stats->alloc_fail = total_stats.rx_alloc_fail;
+}
+
+static const struct netdev_stat_ops rmnet_stat_ops = {
+	.get_queue_stats_rx = rmnet_get_queue_stats_rx,
+};
+
 static const struct net_device_ops rmnet_vnd_ops = {
 	.ndo_start_xmit = rmnet_vnd_start_xmit,
 	.ndo_change_mtu = rmnet_vnd_change_mtu,
@@ -192,8 +235,34 @@ static const char rmnet_gstrings_stats[][ETH_GSTRING_LEN] = {
 	"Checksum skipped",
 	"Checksum computed in software",
 	"Checksum computed in hardware",
+	/* DL coalescing */
+	"Coal alloc packet drops",
+	"Coal frames received",
+	"Packets in coal frames",
+	"Coal header errors",
+	"Coal hdr pkt count errors",
+	"Coal bounds errors",
+	"Coal checksum errors",
+	"Coal packets dropped on csum err",
+	"Coal segments reconstructed",
+	"Coal invalid IP packet",
+	"Coal invalid transport",
+	/* close reasons */
+	"Coal closed: non-coal",
+	"Coal closed: IP miss",
+	"Coal closed: transport miss",
+	"Coal closed: hw NL limit",
+	"Coal closed: hw pkt limit",
+	"Coal closed: hw byte limit",
+	"Coal closed: hw time limit",
+	"Coal closed: hw evict",
+	"Coal closed: FIN/PSH",
 };
 
+static_assert(ARRAY_SIZE(rmnet_gstrings_stats) ==
+	      (sizeof(struct rmnet_priv_stats) -
+	       offsetof(struct rmnet_priv_stats, csum_ok)) / sizeof(u64));
+
 static void rmnet_get_strings(struct net_device *dev, u32 stringset, u8 *buf)
 {
 	switch (stringset) {
@@ -218,12 +287,62 @@ static void rmnet_get_ethtool_stats(struct net_device *dev,
 				    struct ethtool_stats *stats, u64 *data)
 {
 	struct rmnet_priv *priv = netdev_priv(dev);
-	struct rmnet_priv_stats *st = &priv->stats;
+	struct rmnet_priv_stats total_stats = { };
+	struct rmnet_pcpu_stats *pcpu_ptr;
+	struct rmnet_priv_stats snapshot;
+	unsigned int cpu, start;
 
 	if (!data)
 		return;
 
-	memcpy(data, st, ARRAY_SIZE(rmnet_gstrings_stats) * sizeof(u64));
+	for_each_possible_cpu(cpu) {
+		pcpu_ptr = per_cpu_ptr(priv->pcpu_stats, cpu);
+
+		do {
+			start = u64_stats_fetch_begin(&pcpu_ptr->syncp);
+			u64_stats_copy(&snapshot, &pcpu_ptr->priv_stats,
+				       sizeof(snapshot));
+		} while (u64_stats_fetch_retry(&pcpu_ptr->syncp, start));
+
+		total_stats.csum_ok += snapshot.csum_ok;
+		total_stats.csum_ip4_header_bad += snapshot.csum_ip4_header_bad;
+		total_stats.csum_valid_unset += snapshot.csum_valid_unset;
+		total_stats.csum_validation_failed += snapshot.csum_validation_failed;
+		total_stats.csum_err_bad_buffer += snapshot.csum_err_bad_buffer;
+		total_stats.csum_err_invalid_ip_version +=
+			snapshot.csum_err_invalid_ip_version;
+		total_stats.csum_err_invalid_transport +=
+			snapshot.csum_err_invalid_transport;
+		total_stats.csum_fragmented_pkt += snapshot.csum_fragmented_pkt;
+		total_stats.csum_skipped += snapshot.csum_skipped;
+		total_stats.csum_sw += snapshot.csum_sw;
+		total_stats.csum_hw += snapshot.csum_hw;
+		/* Custom rmnet coalescing diagnostics follow. */
+		total_stats.coal_alloc_fail += snapshot.coal_alloc_fail;
+		total_stats.coal_rx += snapshot.coal_rx;
+		total_stats.coal_pkts += snapshot.coal_pkts;
+		total_stats.coal_hdr_err += snapshot.coal_hdr_err;
+		total_stats.coal_hdr_pkt_err += snapshot.coal_hdr_pkt_err;
+		total_stats.coal_bounds_err += snapshot.coal_bounds_err;
+		total_stats.coal_csum_err += snapshot.coal_csum_err;
+		total_stats.coal_csum_drop += snapshot.coal_csum_drop;
+		total_stats.coal_reconstruct += snapshot.coal_reconstruct;
+		total_stats.coal_ip_invalid += snapshot.coal_ip_invalid;
+		total_stats.coal_trans_invalid += snapshot.coal_trans_invalid;
+		total_stats.coal_close_non_coal += snapshot.coal_close_non_coal;
+		total_stats.coal_close_ip_miss += snapshot.coal_close_ip_miss;
+		total_stats.coal_close_trans_miss += snapshot.coal_close_trans_miss;
+		total_stats.coal_close_hw_nl += snapshot.coal_close_hw_nl;
+		total_stats.coal_close_hw_pkt += snapshot.coal_close_hw_pkt;
+		total_stats.coal_close_hw_byte += snapshot.coal_close_hw_byte;
+		total_stats.coal_close_hw_time += snapshot.coal_close_hw_time;
+		total_stats.coal_close_hw_evict += snapshot.coal_close_hw_evict;
+		total_stats.coal_close_coal += snapshot.coal_close_coal;
+	}
+
+	/* The first four fields are reported through netdev queue statistics. */
+	memcpy(data, &total_stats.csum_ok,
+	       sizeof(total_stats) - offsetof(struct rmnet_priv_stats, csum_ok));
 }
 
 static int rmnet_get_coalesce(struct net_device *dev,
@@ -332,6 +451,7 @@ int rmnet_vnd_newlink(u8 id, struct net_device *rmnet_dev,
 	rmnet_dev->hw_features |= NETIF_F_IP_CSUM | NETIF_F_IPV6_CSUM;
 	rmnet_dev->hw_features |= NETIF_F_SG;
 	rmnet_dev->hw_features |= NETIF_F_GRO_HW;
+	rmnet_dev->stat_ops = &rmnet_stat_ops;
 
 	priv->real_dev = real_dev;
 
-- 
2.34.1


  parent reply	other threads:[~2026-10-08  0:57 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 1/8] net: qualcomm: rmnet: Update MTU handling during format changes Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 2/8] uapi: if_link: Add RMNET_FLAGS_INGRESS_COALESCE Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 3/8] net: qualcomm: rmnet: Process MAPv5 frames as a list Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 4/8] net: qualcomm: rmnet: Restrict supported MAP checksum configurations Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 5/8] net: qualcomm: rmnet: Add DL packet coalescing support Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 6/8] net: qualcomm: rmnet: Work around coalescing hardware quirks Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan [this message]
2026-10-08 23:59   ` [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics kernel test robot
2026-10-08  0:55 ` [PATCH net-next v2 8/8] docs: networking: Add documentation for the coalescing support in rmnet Subash Abhinov Kasiviswanathan

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=20261008005543.2630828-8-subash.a.kasiviswanathan@oss.qualcomm.com \
    --to=subash.a.kasiviswanathan@oss.qualcomm.com \
    --cc=andrew+netdev@lunn.ch \
    --cc=corbet@lwn.net \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=horms@kernel.org \
    --cc=kuba@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=rdunlap@infradead.org \
    --cc=sean.tranchetti@oss.qualcomm.com \
    --cc=skhan@linuxfoundation.org \
    /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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox