From f97267ba685f28a2f6e14fd3b25f29ac550faa2c Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Fri, 3 Jul 2020 15:05:52 +0200 Subject: [PATCH] sys/net: add netstats_neighbor --- makefiles/pseudomodules.inc.mk | 5 + sys/Makefile | 3 + sys/Makefile.dep | 5 + sys/include/net/netif.h | 10 +- sys/include/net/netstats.h | 74 +++++ sys/include/net/netstats/neighbor.h | 194 +++++++++++++ sys/net/netstats/Makefile | 3 + sys/net/netstats/netstats_neighbor.c | 400 +++++++++++++++++++++++++++ 8 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 sys/include/net/netstats/neighbor.h create mode 100644 sys/net/netstats/Makefile create mode 100644 sys/net/netstats/netstats_neighbor.c diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index 18652804d4..01d6cd01a1 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -91,6 +91,11 @@ PSEUDOMODULES += netdev_layer PSEUDOMODULES += netdev_register PSEUDOMODULES += netstats PSEUDOMODULES += netstats_l2 +PSEUDOMODULES += netstats_neighbor_etx +PSEUDOMODULES += netstats_neighbor_count +PSEUDOMODULES += netstats_neighbor_rssi +PSEUDOMODULES += netstats_neighbor_lqi +PSEUDOMODULES += netstats_neighbor_tx_time PSEUDOMODULES += netstats_ipv6 PSEUDOMODULES += netstats_rpl PSEUDOMODULES += nimble diff --git a/sys/Makefile b/sys/Makefile index 96942b1585..aaa3528747 100644 --- a/sys/Makefile +++ b/sys/Makefile @@ -107,6 +107,9 @@ endif ifneq (,$(filter netopt,$(USEMODULE))) DIRS += net/crosslayer/netopt endif +ifneq (,$(filter netstats_neighbor,$(USEMODULE))) + DIRS += net/netstats +endif ifneq (,$(filter sema,$(USEMODULE))) DIRS += sema endif diff --git a/sys/Makefile.dep b/sys/Makefile.dep index 18ae9cffbc..1af2827552 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -704,6 +704,11 @@ ifneq (,$(filter netstats_%, $(USEMODULE))) USEMODULE += netstats endif +ifneq (,$(filter netstats_neighbor_%, $(USEMODULE))) + USEMODULE += netstats_neighbor + USEMODULE += xtimer +endif + ifneq (,$(filter gnrc_lwmac,$(USEMODULE))) USEMODULE += gnrc_netif USEMODULE += gnrc_nettype_lwmac diff --git a/sys/include/net/netif.h b/sys/include/net/netif.h index a3fddc6fb3..046d8dc934 100644 --- a/sys/include/net/netif.h +++ b/sys/include/net/netif.h @@ -38,6 +38,11 @@ #include "list.h" #include "net/netopt.h" +#ifdef MODULE_NETSTATS_NEIGHBOR +#include "cib.h" +#include "net/netstats.h" +#endif + #ifdef __cplusplus extern "C" { #endif @@ -61,7 +66,10 @@ extern "C" { * @note All network interfaces should inherit from this structure. */ typedef struct { - list_node_t node; /**< Pointer to the next interface */ + list_node_t node; /**< Pointer to the next interface */ +#ifdef MODULE_NETSTATS_NEIGHBOR + netstats_nb_table_t neighbors; /**< Structure containing all L2 neighbors */ +#endif } netif_t; /** diff --git a/sys/include/net/netstats.h b/sys/include/net/netstats.h index c517b1e3c8..2b19958887 100644 --- a/sys/include/net/netstats.h +++ b/sys/include/net/netstats.h @@ -19,6 +19,8 @@ */ #include +#include "net/l2util.h" +#include "mutex.h" #ifndef NET_NETSTATS_H #define NET_NETSTATS_H @@ -27,6 +29,20 @@ extern "C" { #endif +/** + * @brief The max number of entries in the peer stats table + */ +#ifndef NETSTATS_NB_SIZE +#define NETSTATS_NB_SIZE (8) +#endif + +/** + * @brief The CIB size for tx correlation + */ +#ifndef NETSTATS_NB_QUEUE_SIZE +#define NETSTATS_NB_QUEUE_SIZE (4) +#endif + /** * @name @ref net_netstats module names * @{ @@ -53,6 +69,64 @@ typedef struct { uint32_t rx_bytes; /**< received bytes */ } netstats_t; +/** + * @brief Stats per peer struct + */ +typedef struct { +#if IS_USED(MODULE_NETSTATS_NEIGHBOR_TX_TIME) || DOXYGEN + uint32_t time_tx_avg; /**< Average frame TX time in µs */ +#endif +#if IS_USED(MODULE_NETSTATS_NEIGHBOR_ETX) || DOXYGEN + uint16_t etx; /**< ETX of this peer */ +#endif +#if IS_USED(MODULE_NETSTATS_NEIGHBOR_COUNT) || DOXYGEN + uint16_t tx_count; /**< Number of sent frames to this peer */ + uint16_t tx_fail; /**< Number of sent frames that did not get ACKed */ + uint16_t rx_count; /**< Number of received frames */ +#endif + uint16_t last_updated; /**< seconds timestamp of last update */ + uint16_t last_halved; /**< seconds timestamp of last halving */ + uint8_t l2_addr[L2UTIL_ADDR_MAX_LEN]; /**< Link layer address of the neighbor */ + uint8_t l2_addr_len; /**< Length of netstats_nb::l2_addr */ + uint8_t freshness; /**< Freshness counter */ +#if IS_USED(MODULE_NETSTATS_NEIGHBOR_RSSI) || DOXYGEN + uint8_t rssi; /**< Average RSSI of received frames in abs([dBm]) */ +#endif +#if IS_USED(MODULE_NETSTATS_NEIGHBOR_LQI) || DOXYGEN + uint8_t lqi; /**< Average LQI of received frames */ +#endif +} netstats_nb_t; + +/** + * @brief L2 Peer Info struct + */ +typedef struct { + /** + * @brief CIB for the tx correlation + */ + cib_t stats_idx; + + /** + * @brief send/callback mac association array + */ + netstats_nb_t *stats_queue[NETSTATS_NB_QUEUE_SIZE]; + + /** + * @brief TX timestamp of stats_queue entries + */ + uint32_t stats_queue_time_tx[NETSTATS_NB_QUEUE_SIZE]; + + /** + * @brief Per neighbor statistics array + */ + netstats_nb_t pstats[NETSTATS_NB_SIZE]; + + /** + * @brief Neighbor Table access lock + */ + mutex_t lock; +} netstats_nb_table_t; + #ifdef __cplusplus } #endif diff --git a/sys/include/net/netstats/neighbor.h b/sys/include/net/netstats/neighbor.h new file mode 100644 index 0000000000..35c3c2d1f6 --- /dev/null +++ b/sys/include/net/netstats/neighbor.h @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2017 Koen Zandberg + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @ingroup net_netstats + * @brief Records statistics about link layer neighbors + * @{ + * + * @file + * @brief Neighbor stats definitions + * + * @author Koen Zandberg + */ +#ifndef NET_NETSTATS_NEIGHBOR_H +#define NET_NETSTATS_NEIGHBOR_H + +#include +#include "net/netif.h" +#include "xtimer.h" +#include "timex.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Result of the transmission + * @{ + */ +typedef enum { + NETSTATS_NB_BUSY, /**< Failed due to medium busy */ + NETSTATS_NB_NOACK, /**< Failed due to no ack received */ + NETSTATS_NB_SUCCESS, /**< Successful transmission */ +} netstats_nb_result_t; +/** @} */ + +/** + * @name @ref EWMA parameters + * @{ + */ +/** + * @brief Multiplication factor of the EWMA + */ +#define NETSTATS_NB_EWMA_SCALE 100 + +/** + * @brief Alpha factor of the EWMA + */ +#define NETSTATS_NB_EWMA_ALPHA 15 + +/** + * @brief Alpha factor of the EWMA when stats are not fresh + */ +#define NETSTATS_NB_EWMA_ALPHA_RAMP 30 +/** @} */ + +/** + * @name @ref ETX parameters + * @{ + */ +/** + * @brief ETX penalty for not receiving any ACK + */ +#define NETSTATS_NB_ETX_NOACK_PENALTY 6 +/** + * @brief ETX fixed point divisor (rfc 6551) + */ +#define NETSTATS_NB_ETX_DIVISOR 128 +/** + * @brief Initial ETX, assume a mediocre link + */ +#define NETSTATS_NB_ETX_INIT 2 +/** @} */ + +/** + * @name @ref Freshness parameters + * @{ + */ +/** + * @brief seconds after the freshness counter is halved + */ +#define NETSTATS_NB_FRESHNESS_HALF 600 +/** + * @brief freshness count needed before considering the statistics fresh + */ +#define NETSTATS_NB_FRESHNESS_TARGET 4 +/** + * @brief Maximum freshness + */ +#define NETSTATS_NB_FRESHNESS_MAX 16 +/** + * @brief seconds after statistics have expired + */ +#define NETSTATS_NB_FRESHNESS_EXPIRATION 1200 +/** @} */ +/** + * @name @ref Timeout Parameters + * @{ + */ +/** + * @brief milliseconds without TX done notification after which + * a TX event is discarded + */ +#define NETSTATS_NB_TX_TIMEOUT_MS 100 +/** @} */ + +/** + * @brief Initialize the neighbor stats + * + * @param[in] netif network interface descriptor + * + */ +void netstats_nb_init(netif_t *netif); + +/** + * @brief Find a neighbor stat by the mac address. + * + * @param[in] netif network interface descriptor + * @param[in] l2_addr pointer to the L2 address + * @param[in] len length of the L2 address + * @param[out] out destination for the matching neighbor entry + * + * @return true if a matching peer was found, false otherwise + */ +bool netstats_nb_get(netif_t *netif, const uint8_t *l2_addr, uint8_t len, netstats_nb_t *out); + +/** + * @brief Store this neighbor as next in the transmission queue. + * + * Set @p len to zero if a nop record is needed, for example if the + * transmission has a multicast address as a destination. + * + * @param[in] netif network interface descriptor + * @param[in] l2_addr pointer to the L2 address + * @param[in] len length of the L2 address + * + */ +void netstats_nb_record(netif_t *netif, const uint8_t *l2_addr, uint8_t len); + +/** + * @brief Update the next recorded neighbor with the provided numbers + * + * This only increments the statistics if the length of the l2-address of the retrieved record + * is non-zero. See also @ref netstats_nb_record. The numbers indicate the number of transmissions + * the radio had to perform before a successful transmission was performed. For example: in the case + * of a single send operation needing 3 tries before an ACK was received, there are 2 failed + * transmissions and 1 successful transmission. + * + * @param[in] netif network interface descriptor + * @param[in] result Result of the transmission + * @param[in] transmissions Number of times the packet was sent over the air + * + * @return pointer to the record + */ +netstats_nb_t *netstats_nb_update_tx(netif_t *netif, netstats_nb_result_t result, + uint8_t transmissions); + +/** + * @brief Record rx stats for the l2_addr + * + * @param[in] netif network interface descriptor + * @param[in] l2_addr pointer to the L2 address + * @param[in] l2_addr_len length of the L2 address + * @param[in] rssi RSSI of the received transmission in abs([dBm]) + * @param[in] lqi Link Quality Indication provided by the radio + * + * @return pointer to the updated record + */ +netstats_nb_t *netstats_nb_update_rx(netif_t *netif, const uint8_t *l2_addr, + uint8_t l2_addr_len, uint8_t rssi, uint8_t lqi); + +/** + * @brief Check if a record is fresh + * + * Freshness half time is checked and updated before verifying freshness. + * + * @param[in] netif network interface the statistic belongs to + * @param[in] stats pointer to the statistic + */ +bool netstats_nb_isfresh(netif_t *netif, netstats_nb_t *stats); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_NETSTATS_NEIGHBOR_H */ +/** + * @} + */ diff --git a/sys/net/netstats/Makefile b/sys/net/netstats/Makefile new file mode 100644 index 0000000000..adb30c19ac --- /dev/null +++ b/sys/net/netstats/Makefile @@ -0,0 +1,3 @@ +MODULE = netstats_neighbor + +include $(RIOTBASE)/Makefile.base diff --git a/sys/net/netstats/netstats_neighbor.c b/sys/net/netstats/netstats_neighbor.c new file mode 100644 index 0000000000..2c95b6ddba --- /dev/null +++ b/sys/net/netstats/netstats_neighbor.c @@ -0,0 +1,400 @@ +/* + * Copyright (C) Koen Zandberg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * @ingroup net + * @file + * @brief Neighbor level stats for netdev + * + * @author Koen Zandberg + * @author Benjamin Valentin + * @} + */ + +#include + +#include "net/l2util.h" +#include "net/netdev.h" +#include "net/netstats/neighbor.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +static inline void _lock(netif_t *dev) +{ + mutex_lock(&dev->neighbors.lock); +} + +static inline void _unlock(netif_t *dev) +{ + mutex_unlock(&dev->neighbors.lock); +} + +/** + * @brief Compare the freshness of two records + * + * @param[in] a pointer to the first record + * @param[in] b pointer to the second record + * @param[in] now current timestamp in seconds + * + * @return pointer to the least fresh record + */ +static inline netstats_nb_t *netstats_nb_comp(const netstats_nb_t *a, + const netstats_nb_t *b, + uint16_t now) +{ + return (netstats_nb_t *)(((now - a->last_updated) > now - b->last_updated) ? a : b); +} + +static void half_freshness(netstats_nb_t *stats, uint16_t now_sec) +{ + uint8_t diff = (now_sec - stats->last_halved) / NETSTATS_NB_FRESHNESS_HALF; + stats->freshness >>= diff; + + if (diff) { + /* Set to the last time point where this should have been halved */ + stats->last_halved = now_sec - diff; + } +} + +static void incr_freshness(netstats_nb_t *stats) +{ + uint16_t now = xtimer_now_usec() / US_PER_SEC;; + + /* First halve the freshness if applicable */ + half_freshness(stats, now); + + /* Increment the freshness capped at FRESHNESS_MAX */ + if (stats->freshness < NETSTATS_NB_FRESHNESS_MAX) { + stats->freshness++; + } + + stats->last_updated = now; +} + +static bool isfresh(netstats_nb_t *stats) +{ + uint16_t now = xtimer_now_usec() / US_PER_SEC; + + /* Half freshness if applicable to update to current freshness */ + half_freshness(stats, now); + + return (stats->freshness >= NETSTATS_NB_FRESHNESS_TARGET) && + (now - stats->last_updated < NETSTATS_NB_FRESHNESS_EXPIRATION); +} + +bool netstats_nb_isfresh(netif_t *dev, netstats_nb_t *stats) +{ + bool ret; + + _lock(dev); + ret = isfresh(stats); + _unlock(dev); + + return ret; +} + +void netstats_nb_init(netif_t *dev) +{ + mutex_init(&dev->neighbors.lock); + + _lock(dev); + memset(dev->neighbors.pstats, 0, sizeof(netstats_nb_t) * NETSTATS_NB_SIZE); + cib_init(&dev->neighbors.stats_idx, NETSTATS_NB_QUEUE_SIZE); + _unlock(dev); +} + +static void netstats_nb_create(netstats_nb_t *entry, const uint8_t *l2_addr, uint8_t l2_len) +{ + memset(entry, 0, sizeof(netstats_nb_t)); + memcpy(entry->l2_addr, l2_addr, l2_len); + entry->l2_addr_len = l2_len; + +#ifdef MODULE_NETSTATS_NEIGHBOR_ETX + entry->etx = NETSTATS_NB_ETX_INIT * NETSTATS_NB_ETX_DIVISOR; +#endif +} + +bool netstats_nb_get(netif_t *dev, const uint8_t *l2_addr, uint8_t len, netstats_nb_t *out) +{ + _lock(dev); + + netstats_nb_t *stats = dev->neighbors.pstats; + bool found = false; + + for (int i = 0; i < NETSTATS_NB_SIZE; i++) { + + /* Check if this is the matching entry */ + if (l2util_addr_equal(stats[i].l2_addr, stats[i].l2_addr_len, l2_addr, len)) { + *out = stats[i]; + found = true; + break; + } + } + + _unlock(dev); + return found; +} + +/* find the oldest inactive entry to replace. Empty entries are infinity old */ +static netstats_nb_t *netstats_nb_get_or_create(netif_t *dev, const uint8_t *l2_addr, uint8_t len) +{ + netstats_nb_t *old_entry = NULL; + netstats_nb_t *stats = dev->neighbors.pstats; + uint16_t now = xtimer_now_usec() / US_PER_SEC; + + for (int i = 0; i < NETSTATS_NB_SIZE; i++) { + + /* Check if this is the matching entry */ + if (l2util_addr_equal(stats[i].l2_addr, stats[i].l2_addr_len, l2_addr, len)) { + return &stats[i]; + } + + /* Entry is oldest if it is empty */ + if (stats[i].l2_addr_len == 0) { + old_entry = &stats[i]; + } + /* Check if the entry is expired */ + else if (!isfresh(&stats[i])) { + /* Entry is oldest if it is expired */ + if (old_entry == NULL) { + old_entry = &stats[i]; + } + /* don't replace old entry if there are still empty ones */ + else if (old_entry->l2_addr_len > 0) { + /* Check if current entry is older than current oldest entry */ + old_entry = netstats_nb_comp(old_entry, &stats[i], now); + } + } + } + + /* if there is no matching entry, + * create a new entry if we have an expired one */ + if (old_entry) { + netstats_nb_create(old_entry, l2_addr, len); + } + + return old_entry; +} + +void netstats_nb_record(netif_t *dev, const uint8_t *l2_addr, uint8_t len) +{ + _lock(dev); + + int idx = cib_put(&dev->neighbors.stats_idx); + + if (idx < 0) { + DEBUG("%s: put buffer empty\n", __func__); + goto out; + } + + DEBUG("put %d\n", idx); + + if (len == 0) { + /* Fill queue with a NOP */ + dev->neighbors.stats_queue[idx] = NULL; + } else { + dev->neighbors.stats_queue[idx] = netstats_nb_get_or_create(dev, l2_addr, len); + dev->neighbors.stats_queue_time_tx[idx] = xtimer_now_usec(); + } + +out: + _unlock(dev); +} + +/* Get the first available neighbor in the transmission queue + * and increment pointer. */ +static netstats_nb_t *netstats_nb_get_recorded(netif_t *dev, uint32_t *time_tx) +{ + netstats_nb_t *res; + int idx = cib_get(&dev->neighbors.stats_idx); + + if (idx < 0) { + DEBUG("%s: can't get record\n", __func__); + return NULL; + } + + DEBUG("get %d (%d left)\n", idx, cib_avail(&dev->neighbors.stats_idx)); + + res = dev->neighbors.stats_queue[idx]; + dev->neighbors.stats_queue[idx] = NULL; + + *time_tx = dev->neighbors.stats_queue_time_tx[idx]; + + return res; +} + +__attribute__((unused)) +static uint32_t _ewma(bool fresh, uint32_t old_val, uint32_t new_val) +{ + uint8_t ewma_alpha; + + if (old_val == 0) { + return new_val; + } + + /* If the stats are not fresh, use a larger alpha to average aggressive */ + if (fresh) { + ewma_alpha = NETSTATS_NB_EWMA_ALPHA; + } else { + ewma_alpha = NETSTATS_NB_EWMA_ALPHA_RAMP; + } + + /* Exponential weighted moving average */ + return (old_val * (NETSTATS_NB_EWMA_SCALE - ewma_alpha) + + new_val * ewma_alpha) / NETSTATS_NB_EWMA_SCALE; +} + +static void netstats_nb_update_etx(netstats_nb_t *stats, netstats_nb_result_t result, + uint8_t transmissions, bool fresh) +{ + /* don't do anything if driver does not report ETX */ + if (transmissions == 0) { + return; + } + + if (result != NETSTATS_NB_SUCCESS) { + transmissions = NETSTATS_NB_ETX_NOACK_PENALTY; + } + +#ifdef MODULE_NETSTATS_NEIGHBOR_ETX + stats->etx = _ewma(fresh, stats->etx, transmissions * NETSTATS_NB_ETX_DIVISOR); +#else + (void)stats; + (void)result; + (void)transmissions; + (void)fresh; +#endif +} + +static void netstats_nb_update_time(netstats_nb_t *stats, netstats_nb_result_t result, + uint32_t duration, bool fresh) +{ + /* TX time already got a penalty due to retransmissions */ + if (result != NETSTATS_NB_SUCCESS) { + duration *= 2; + } + +#if MODULE_NETSTATS_NEIGHBOR_TX_TIME + stats->time_tx_avg = _ewma(fresh, stats->time_tx_avg, duration); +#else + (void)stats; + (void)result; + (void)duration; + (void)fresh; +#endif +} + +static void netstats_nb_update_rssi(netstats_nb_t *stats, uint8_t rssi, bool fresh) +{ +#ifdef MODULE_NETSTATS_NEIGHBOR_RSSI + stats->rssi = _ewma(fresh, stats->rssi, rssi); +#else + (void)stats; + (void)rssi; + (void)fresh; +#endif +} + +static void netstats_nb_update_lqi(netstats_nb_t *stats, uint8_t lqi, bool fresh) +{ +#ifdef MODULE_NETSTATS_NEIGHBOR_LQI + stats->lqi = _ewma(fresh, stats->lqi, lqi); +#else + (void)stats; + (void)lqi; + (void)fresh; +#endif +} + +static void netstats_nb_incr_count_tx(netstats_nb_t *stats, netstats_nb_result_t result) +{ +#ifdef MODULE_NETSTATS_NEIGHBOR_COUNT + stats->tx_count++; + + /* gracefully handle overflow */ + if (stats->tx_count == 0) { + stats->tx_count = ~stats->tx_count; + stats->tx_count = stats->tx_count >> 4; + stats->tx_fail = stats->tx_fail >> 4; + } + + if (result != NETSTATS_NB_SUCCESS) { + stats->tx_fail++; + } +#else + (void)stats; + (void)result; +#endif +} + +static void netstats_nb_incr_count_rx(netstats_nb_t *stats) +{ +#ifdef MODULE_NETSTATS_NEIGHBOR_COUNT + stats->rx_count++; +#else + (void)stats; +#endif +} + +netstats_nb_t *netstats_nb_update_tx(netif_t *dev, netstats_nb_result_t result, + uint8_t transmissions) +{ + uint32_t now = xtimer_now_usec(); + netstats_nb_t *stats; + uint32_t time_tx = 0; + + _lock(dev); + + /* Buggy drivers don't always generate TX done events. + * Discard old events to prevent the tx start <-> tx done correlation + * from getting out of sync. */ + do { + stats = netstats_nb_get_recorded(dev, &time_tx); + } while (cib_avail(&dev->neighbors.stats_idx) + && ((now - time_tx) > NETSTATS_NB_TX_TIMEOUT_MS * US_PER_MS)); + + /* Nothing to do for multicast or if packet was not sent */ + if (result == NETSTATS_NB_BUSY || stats == NULL) { + goto out; + } + + bool fresh = isfresh(stats); + + netstats_nb_update_time(stats, result, now - time_tx, fresh); + netstats_nb_update_etx(stats, result, transmissions, fresh); + netstats_nb_incr_count_tx(stats, result); + + incr_freshness(stats); + +out: + _unlock(dev); + return stats; +} + +netstats_nb_t *netstats_nb_update_rx(netif_t *dev, const uint8_t *l2_addr, + uint8_t l2_addr_len, uint8_t rssi, uint8_t lqi) +{ + _lock(dev); + + netstats_nb_t *stats = netstats_nb_get_or_create(dev, l2_addr, l2_addr_len); + + if (stats != NULL) { + bool fresh = isfresh(stats); + + netstats_nb_update_rssi(stats, rssi, fresh); + netstats_nb_update_lqi(stats, lqi, fresh); + netstats_nb_incr_count_rx(stats); + + incr_freshness(stats); + } + + _unlock(dev); + return stats; +}