src/stats.h

Device stats structs.

Walkthrough, interview notes & deep dive

stats.h provides the interface for performance monitoring and telemetry within the ixy driver. In high-performance networking, especially when targeting low-latency environments like those powered by Solarflare hardware, accurate and low-overhead statistics collection is vital for identifying bottlenecks, packet loss, and throughput limits. This header defines the structures and function prototypes necessary to track and report packet processing metrics.

The core of this file is the struct device_stats declaration. It encapsulates the operational state of a network interface from a performance perspective. The structure contains a pointer to the ixy_device, followed by four primary counters: rx_pkts and tx_pkts for packet counts, and rx_bytes and tx_bytes for total byte throughput.

struct device_stats {
	struct ixy_device* device;
	size_t rx_pkts;
	size_t tx_pkts;
	size_t rx_bytes;
	size_t tx_bytes;
};

The supporting functions facilitate the lifecycle and reporting of these metrics. stats_init initializes a statistics structure and associates it with a specific device. For reporting, print_stats provides a snapshot of cumulative counters, while print_stats_diff is used to calculate and display the delta between two samples. This difference is paired with a nanos_passed value to calculate rates such as Million Packets Per Second (Mpps) or Gigabits Per Second (Gbps).

The utility function uint64_t monotonic_time() is crucial for performance measurement. It typically wraps a high-resolution timer (like clock_gettime with CLOCK_MONOTONIC) to ensure that interval calculations are not affected by system clock jumps or NTP adjustments.

In a real-world driver implementation, these stats are often updated in the "fast path"β€”the inner loops of the RX and TX functions. While this header defines the storage, the underlying mechanism relies on the driver logic to increment these size_t counters every time a batch of packets is successfully polled from or cleaned up on the descriptor rings.

Interview angles

Why use a monotonic clock instead of wall-clock time for statistics? Wall-clock time can jump forward or backward due to NTP syncs or manual adjustments. For calculating throughput (packets/time), a monotonic clock ensures that the denominator is always increasing and consistent, preventing negative rates or division-by-zero errors during interval calculations.

What are the concurrency implications of updating `struct device_stats`? In a multi-queue environment (RSS), multiple CPU cores might attempt to update statistics simultaneously. If they update the same device_stats struct, it can lead to "false sharing" where cache lines bounce between cores, severely degrading performance. Professional drivers often use per-core or per-queue statistics that are only aggregated during the reporting phase.

Why are these counters often 64-bit even on 32-bit systems? At 100GbE speeds, a 32-bit byte counter (roughly 4GB) can wrap around in less than a second. Using 64-bit identifiers like uint64_t or size_t (on 64-bit architectures) ensures that the counters can run for years without overflowing, which is critical for long-term telemetry and SLA monitoring.

Going deeper

struct device_stats {
	struct ixy_device* device;
	size_t rx_pkts;
	size_t tx_pkts;
	size_t rx_bytes;
	size_t tx_bytes;
};

The device pointer as the first member ensures the handle is co-located with the counters, but this layout is a false-sharing magnet. If one core bumps rx_pkts while another thread reads the struct to update a UI, the cache line bounces between cores. Using size_t for byte counters is an ABI trap: on 32-bit systems, rx_bytes will wrap in roughly 3.4 seconds at 10GbE line rate.

uint64_t monotonic_time();

In C, the empty parentheses in monotonic_time() mean "unspecified arguments" rather than "void." This bypasses prototype checking, allowing a caller to pass arbitrary arguments without a compiler warning, potentially leading to stack corruption in older calling conventions.

Harder interview questions

  • "How would you calculate Mpps and Gbps from these stats without using floating point?" You multiply the delta by 10^9 before dividing by nanos_passed, but you must check for overflow first. For Gbps, you also factor in the 20-byte Ethernet framing overhead (preamble/IFG) per packet to get "wire speed" rather than just "payload speed."
  • "What happens if a device is reset or a queue is reconfigured between two calls to print_stats_diff?" If the hardware registers reset to zero, stats_new will be less than stats_old, resulting in a massive underflow when using unsigned size_t math, leading to nonsensical throughput reporting.

Gotchas

  • The lack of const on the stats_new and stats_old pointers in print_stats_diff is a design flaw that prevents passing immutable snapshots or telemetry from read-only segments.
  • Increments like stats->rx_pkts++ on the fast path are not atomic. In a multi-queue scenario where multiple RX threads share a device_stats struct, increments will be lost due to standard read-modify-write races.

From ixy to a production driver

In stats.h, ixy treats telemetry as a teaching tool: struct device_stats stores four size_t software counters, the RX/TX fast path increments them, and print_stats_diff() turns deltas over monotonic_time() into Mpps and Gbps. A production driver for the same Intel 82599-class hardware does not use that as its primary truth. Linux ixgbe.ko calls ixgbe_update_stats() and folds hardware register values into netdev statistics, surfaced through rtnl_link_stats64, /proc/net/dev, sysfs, and driver-specific ethtool -S names.

Hardware is the source of truth because the NIC sees events the CPU never processes. The 82599 has counters such as GPRC and GPTC for good packets received/transmitted, GORCL/GORCH and GOTCL/GOTCH for octet counts split into low/high halves, TPR and TPT for total packets, per-queue QPRC, and MPC for missed packets. MPC is exactly the kind of number ixy omits: packets dropped in hardware because receive resources were unavailable. An interviewer expects you to notice that ixy's rx_pkts++ counts packets returned to the application, not packets lost before descriptor completion.

Counter mechanics are real driver work. Hardware statistics registers may be accumulating, clear-on-read, narrower than the exported 64-bit value, or split into low/high registers. A real driver has to read them in order, accumulate software shadows, handle wraparound, and avoid losing events during reset or link changes. ixy sidesteps that, which is the right educational tradeoff: the file demonstrates rate math and monotonic intervals without MAC-specific accounting rules.

The hot path scales differently. ixy has one device_stats object with non-atomic size_t fields, so multiqueue RSS would race or create cache-line contention if several cores updated it directly. Linux drivers normally keep per-queue or per-CPU counters and aggregate at read time; on 32-bit systems, helpers such as u64_stats_sync protect readers from torn 64-bit updates. DPDK follows the same shape from user space: rte_eth_stats_get() fills struct rte_eth_stats, including totals plus per-queue fields such as q_ipackets and q_ibytes, while rte_eth_xstats_get() exposes extended NIC-specific counters. DPDK ixgbe documentation says the hardware statistics must be polled regularly with those APIs so registers do not saturate and stick.

Virtual devices still have a stats model. Linux virtio_net has struct virtnet_stats-style per-queue accounting, ethtool statistics, and virtio feature negotiation such as VIRTIO_NET_F_CTRL_GUEST_OFFLOADS for control-plane capabilities. The lesson is the same as ixgbe, just with a virtual device boundary: production telemetry is a contract between hardware or hypervisor, driver, and userspace tooling. ixy gives you only the minimal counters needed to explain throughput; the missing pieces are the production concerns to name in an interview.

Sources

Source

filesrc/stats.h
#ifndef IXY_STATS_H
#define IXY_STATS_H

#include <stdint.h>
#include <stddef.h>
#include <time.h>
#include "driver/device.h"

struct device_stats {
	struct ixy_device* device;
	size_t rx_pkts;
	size_t tx_pkts;
	size_t rx_bytes;
	size_t tx_bytes;
};



void print_stats(struct device_stats* stats);
void print_stats_diff(struct device_stats* stats_new, struct device_stats* stats_old, uint64_t nanos_passed);
void stats_init(struct device_stats* stats, struct ixy_device* dev);

uint64_t monotonic_time();

#endif //IXY_STATS_H