root/tools/net/ynl/ynltool/qstats.c
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <net/if.h>
#include <math.h>

#include <ynl.h>
#include "netdev-user.h"

#include "main.h"

static enum netdev_qstats_scope scope; /* default - device */

struct queue_balance {
        unsigned int ifindex;
        enum netdev_queue_type type;
        unsigned int queue_count;
        __u64 *rx_packets;
        __u64 *rx_bytes;
        __u64 *tx_packets;
        __u64 *tx_bytes;
};

static void print_json_qstats(struct netdev_qstats_get_list *qstats)
{
        jsonw_start_array(json_wtr);

        ynl_dump_foreach(qstats, qs) {
                char ifname[IF_NAMESIZE];
                const char *name;

                jsonw_start_object(json_wtr);

                name = if_indextoname(qs->ifindex, ifname);
                if (name)
                        jsonw_string_field(json_wtr, "ifname", name);
                jsonw_uint_field(json_wtr, "ifindex", qs->ifindex);

                if (qs->_present.queue_type)
                        jsonw_string_field(json_wtr, "queue-type",
                                           netdev_queue_type_str(qs->queue_type));
                if (qs->_present.queue_id)
                        jsonw_uint_field(json_wtr, "queue-id", qs->queue_id);

                if (qs->_present.rx_packets || qs->_present.rx_bytes ||
                    qs->_present.rx_alloc_fail || qs->_present.rx_hw_drops ||
                    qs->_present.rx_csum_complete || qs->_present.rx_hw_gro_packets) {
                        jsonw_name(json_wtr, "rx");
                        jsonw_start_object(json_wtr);
                        if (qs->_present.rx_packets)
                                jsonw_uint_field(json_wtr, "packets", qs->rx_packets);
                        if (qs->_present.rx_bytes)
                                jsonw_uint_field(json_wtr, "bytes", qs->rx_bytes);
                        if (qs->_present.rx_alloc_fail)
                                jsonw_uint_field(json_wtr, "alloc-fail", qs->rx_alloc_fail);
                        if (qs->_present.rx_hw_drops)
                                jsonw_uint_field(json_wtr, "hw-drops", qs->rx_hw_drops);
                        if (qs->_present.rx_hw_drop_overruns)
                                jsonw_uint_field(json_wtr, "hw-drop-overruns", qs->rx_hw_drop_overruns);
                        if (qs->_present.rx_hw_drop_ratelimits)
                                jsonw_uint_field(json_wtr, "hw-drop-ratelimits", qs->rx_hw_drop_ratelimits);
                        if (qs->_present.rx_csum_complete)
                                jsonw_uint_field(json_wtr, "csum-complete", qs->rx_csum_complete);
                        if (qs->_present.rx_csum_unnecessary)
                                jsonw_uint_field(json_wtr, "csum-unnecessary", qs->rx_csum_unnecessary);
                        if (qs->_present.rx_csum_none)
                                jsonw_uint_field(json_wtr, "csum-none", qs->rx_csum_none);
                        if (qs->_present.rx_csum_bad)
                                jsonw_uint_field(json_wtr, "csum-bad", qs->rx_csum_bad);
                        if (qs->_present.rx_hw_gro_packets)
                                jsonw_uint_field(json_wtr, "hw-gro-packets", qs->rx_hw_gro_packets);
                        if (qs->_present.rx_hw_gro_bytes)
                                jsonw_uint_field(json_wtr, "hw-gro-bytes", qs->rx_hw_gro_bytes);
                        if (qs->_present.rx_hw_gro_wire_packets)
                                jsonw_uint_field(json_wtr, "hw-gro-wire-packets", qs->rx_hw_gro_wire_packets);
                        if (qs->_present.rx_hw_gro_wire_bytes)
                                jsonw_uint_field(json_wtr, "hw-gro-wire-bytes", qs->rx_hw_gro_wire_bytes);
                        jsonw_end_object(json_wtr);
                }

                if (qs->_present.tx_packets || qs->_present.tx_bytes ||
                    qs->_present.tx_hw_drops || qs->_present.tx_csum_none ||
                    qs->_present.tx_hw_gso_packets) {
                        jsonw_name(json_wtr, "tx");
                        jsonw_start_object(json_wtr);
                        if (qs->_present.tx_packets)
                                jsonw_uint_field(json_wtr, "packets", qs->tx_packets);
                        if (qs->_present.tx_bytes)
                                jsonw_uint_field(json_wtr, "bytes", qs->tx_bytes);
                        if (qs->_present.tx_hw_drops)
                                jsonw_uint_field(json_wtr, "hw-drops", qs->tx_hw_drops);
                        if (qs->_present.tx_hw_drop_errors)
                                jsonw_uint_field(json_wtr, "hw-drop-errors", qs->tx_hw_drop_errors);
                        if (qs->_present.tx_hw_drop_ratelimits)
                                jsonw_uint_field(json_wtr, "hw-drop-ratelimits", qs->tx_hw_drop_ratelimits);
                        if (qs->_present.tx_csum_none)
                                jsonw_uint_field(json_wtr, "csum-none", qs->tx_csum_none);
                        if (qs->_present.tx_needs_csum)
                                jsonw_uint_field(json_wtr, "needs-csum", qs->tx_needs_csum);
                        if (qs->_present.tx_hw_gso_packets)
                                jsonw_uint_field(json_wtr, "hw-gso-packets", qs->tx_hw_gso_packets);
                        if (qs->_present.tx_hw_gso_bytes)
                                jsonw_uint_field(json_wtr, "hw-gso-bytes", qs->tx_hw_gso_bytes);
                        if (qs->_present.tx_hw_gso_wire_packets)
                                jsonw_uint_field(json_wtr, "hw-gso-wire-packets", qs->tx_hw_gso_wire_packets);
                        if (qs->_present.tx_hw_gso_wire_bytes)
                                jsonw_uint_field(json_wtr, "hw-gso-wire-bytes", qs->tx_hw_gso_wire_bytes);
                        if (qs->_present.tx_stop)
                                jsonw_uint_field(json_wtr, "stop", qs->tx_stop);
                        if (qs->_present.tx_wake)
                                jsonw_uint_field(json_wtr, "wake", qs->tx_wake);
                        jsonw_end_object(json_wtr);
                }

                jsonw_end_object(json_wtr);
        }

        jsonw_end_array(json_wtr);
}

static void print_one(bool present, const char *name, unsigned long long val,
                      int *line)
{
        if (!present)
                return;

        if (!*line) {
                printf("              ");
                ++(*line);
        }

        /* Don't waste space on tx- and rx- prefix, its implied by queue type */
        if (scope == NETDEV_QSTATS_SCOPE_QUEUE &&
            (name[0] == 'r' || name[0] == 't') &&
            name[1] == 'x' && name[2] == '-')
                name += 3;

        printf(" %15s: %15llu", name, val);

        if (++(*line) == 3) {
                printf("\n");
                *line = 0;
        }
}

static void print_plain_qstats(struct netdev_qstats_get_list *qstats)
{
        ynl_dump_foreach(qstats, qs) {
                char ifname[IF_NAMESIZE];
                const char *name;
                int n;

                name = if_indextoname(qs->ifindex, ifname);
                if (name)
                        printf("%s", name);
                else
                        printf("ifindex:%u", qs->ifindex);

                if (qs->_present.queue_type && qs->_present.queue_id)
                        printf("\t%s-%-3u",
                               netdev_queue_type_str(qs->queue_type),
                               qs->queue_id);
                else
                        printf("\t      ");

                n = 1;

                /* Basic counters */
                print_one(qs->_present.rx_packets, "rx-packets", qs->rx_packets, &n);
                print_one(qs->_present.rx_bytes, "rx-bytes", qs->rx_bytes, &n);
                print_one(qs->_present.tx_packets, "tx-packets", qs->tx_packets, &n);
                print_one(qs->_present.tx_bytes, "tx-bytes", qs->tx_bytes, &n);

                /* RX error/drop counters */
                print_one(qs->_present.rx_alloc_fail, "rx-alloc-fail",
                          qs->rx_alloc_fail, &n);
                print_one(qs->_present.rx_hw_drops, "rx-hw-drops",
                          qs->rx_hw_drops, &n);
                print_one(qs->_present.rx_hw_drop_overruns, "rx-hw-drop-overruns",
                          qs->rx_hw_drop_overruns, &n);
                print_one(qs->_present.rx_hw_drop_ratelimits, "rx-hw-drop-ratelimits",
                          qs->rx_hw_drop_ratelimits, &n);

                /* RX checksum counters */
                print_one(qs->_present.rx_csum_complete, "rx-csum-complete",
                          qs->rx_csum_complete, &n);
                print_one(qs->_present.rx_csum_unnecessary, "rx-csum-unnecessary",
                          qs->rx_csum_unnecessary, &n);
                print_one(qs->_present.rx_csum_none, "rx-csum-none",
                          qs->rx_csum_none, &n);
                print_one(qs->_present.rx_csum_bad, "rx-csum-bad",
                          qs->rx_csum_bad, &n);

                /* RX GRO counters */
                print_one(qs->_present.rx_hw_gro_packets, "rx-hw-gro-packets",
                          qs->rx_hw_gro_packets, &n);
                print_one(qs->_present.rx_hw_gro_bytes, "rx-hw-gro-bytes",
                          qs->rx_hw_gro_bytes, &n);
                print_one(qs->_present.rx_hw_gro_wire_packets, "rx-hw-gro-wire-packets",
                          qs->rx_hw_gro_wire_packets, &n);
                print_one(qs->_present.rx_hw_gro_wire_bytes, "rx-hw-gro-wire-bytes",
                          qs->rx_hw_gro_wire_bytes, &n);

                /* TX error/drop counters */
                print_one(qs->_present.tx_hw_drops, "tx-hw-drops",
                          qs->tx_hw_drops, &n);
                print_one(qs->_present.tx_hw_drop_errors, "tx-hw-drop-errors",
                          qs->tx_hw_drop_errors, &n);
                print_one(qs->_present.tx_hw_drop_ratelimits, "tx-hw-drop-ratelimits",
                          qs->tx_hw_drop_ratelimits, &n);

                /* TX checksum counters */
                print_one(qs->_present.tx_csum_none, "tx-csum-none",
                          qs->tx_csum_none, &n);
                print_one(qs->_present.tx_needs_csum, "tx-needs-csum",
                          qs->tx_needs_csum, &n);

                /* TX GSO counters */
                print_one(qs->_present.tx_hw_gso_packets, "tx-hw-gso-packets",
                          qs->tx_hw_gso_packets, &n);
                print_one(qs->_present.tx_hw_gso_bytes, "tx-hw-gso-bytes",
                          qs->tx_hw_gso_bytes, &n);
                print_one(qs->_present.tx_hw_gso_wire_packets, "tx-hw-gso-wire-packets",
                          qs->tx_hw_gso_wire_packets, &n);
                print_one(qs->_present.tx_hw_gso_wire_bytes, "tx-hw-gso-wire-bytes",
                          qs->tx_hw_gso_wire_bytes, &n);

                /* TX queue control */
                print_one(qs->_present.tx_stop, "tx-stop", qs->tx_stop, &n);
                print_one(qs->_present.tx_wake, "tx-wake", qs->tx_wake, &n);

                if (n)
                        printf("\n");
        }
}

static struct netdev_qstats_get_list *
qstats_dump(enum netdev_qstats_scope scope)
{
        struct netdev_qstats_get_list *qstats;
        struct netdev_qstats_get_req *req;
        struct ynl_error yerr;
        struct ynl_sock *ys;

        ys = ynl_sock_create(&ynl_netdev_family, &yerr);
        if (!ys) {
                p_err("YNL: %s", yerr.msg);
                return NULL;
        }

        req = netdev_qstats_get_req_alloc();
        if (!req) {
                p_err("failed to allocate qstats request");
                goto err_close;
        }

        if (scope)
                netdev_qstats_get_req_set_scope(req, scope);

        qstats = netdev_qstats_get_dump(ys, req);
        netdev_qstats_get_req_free(req);
        if (!qstats) {
                p_err("failed to get queue stats: %s", ys->err.msg);
                goto err_close;
        }

        ynl_sock_destroy(ys);
        return qstats;

err_close:
        ynl_sock_destroy(ys);
        return NULL;
}

static int do_show(int argc, char **argv)
{
        struct netdev_qstats_get_list *qstats;

        /* Parse options */
        while (argc > 0) {
                if (is_prefix(*argv, "scope") || is_prefix(*argv, "group-by")) {
                        NEXT_ARG();

                        if (!REQ_ARGS(1))
                                return -1;

                        if (is_prefix(*argv, "queue")) {
                                scope = NETDEV_QSTATS_SCOPE_QUEUE;
                        } else if (is_prefix(*argv, "device")) {
                                scope = 0;
                        } else {
                                p_err("invalid scope value '%s'", *argv);
                                return -1;
                        }
                        NEXT_ARG();
                } else {
                        p_err("unknown option '%s'", *argv);
                        return -1;
                }
        }

        qstats = qstats_dump(scope);
        if (!qstats)
                return -1;

        /* Print the stats as returned by the kernel */
        if (json_output)
                print_json_qstats(qstats);
        else
                print_plain_qstats(qstats);

        netdev_qstats_get_list_free(qstats);
        return 0;
}

static void compute_stats(__u64 *values, unsigned int count,
                          double *mean, double *stddev, __u64 *min, __u64 *max)
{
        double sum = 0.0, variance = 0.0;
        unsigned int i;

        *min = ~0ULL;
        *max = 0;

        if (count == 0) {
                *mean = 0;
                *stddev = 0;
                *min = 0;
                return;
        }

        for (i = 0; i < count; i++) {
                sum += values[i];
                if (values[i] < *min)
                        *min = values[i];
                if (values[i] > *max)
                        *max = values[i];
        }

        *mean = sum / count;

        if (count > 1) {
                for (i = 0; i < count; i++) {
                        double diff = values[i] - *mean;

                        variance += diff * diff;
                }
                *stddev = sqrt(variance / (count - 1));
        } else {
                *stddev = 0;
        }
}

static void print_balance_stats(const char *name, enum netdev_queue_type type,
                                __u64 *values, unsigned int count)
{
        double mean, stddev, cv, ns;
        __u64 min, max;

        if ((name[0] == 'r' && type != NETDEV_QUEUE_TYPE_RX) ||
            (name[0] == 't' && type != NETDEV_QUEUE_TYPE_TX))
                return;

        compute_stats(values, count, &mean, &stddev, &min, &max);

        cv = mean > 0 ? (stddev / mean) * 100.0 : 0.0;
        ns = min + max > 0 ? (double)2 * (max - min) / (max + min) * 100 : 0.0;

        printf("  %-12s: cv=%.1f%% ns=%.1f%% stddev=%.0f\n",
               name, cv, ns, stddev);
        printf("  %-12s  min=%llu max=%llu mean=%.0f\n",
               "", min, max, mean);
}

static void
print_balance_stats_json(const char *name, enum netdev_queue_type type,
                         __u64 *values, unsigned int count)
{
        double mean, stddev, cv, ns;
        __u64 min, max;

        if ((name[0] == 'r' && type != NETDEV_QUEUE_TYPE_RX) ||
            (name[0] == 't' && type != NETDEV_QUEUE_TYPE_TX))
                return;

        compute_stats(values, count, &mean, &stddev, &min, &max);

        cv = mean > 0 ? (stddev / mean) * 100.0 : 0.0;
        ns = min + max > 0 ? (double)2 * (max - min) / (max + min) * 100 : 0.0;

        jsonw_name(json_wtr, name);
        jsonw_start_object(json_wtr);
        jsonw_uint_field(json_wtr, "queue-count", count);
        jsonw_uint_field(json_wtr, "min", min);
        jsonw_uint_field(json_wtr, "max", max);
        jsonw_float_field(json_wtr, "mean", mean);
        jsonw_float_field(json_wtr, "stddev", stddev);
        jsonw_float_field(json_wtr, "coefficient-of-variation", cv);
        jsonw_float_field(json_wtr, "normalized-spread", ns);
        jsonw_end_object(json_wtr);
}

static int cmp_ifindex_type(const void *a, const void *b)
{
        const struct netdev_qstats_get_rsp *qa = a;
        const struct netdev_qstats_get_rsp *qb = b;

        if (qa->ifindex != qb->ifindex)
                return qa->ifindex - qb->ifindex;
        if (qa->queue_type != qb->queue_type)
                return qa->queue_type - qb->queue_type;
        return qa->queue_id - qb->queue_id;
}

static int do_balance(int argc, char **argv __attribute__((unused)))
{
        struct netdev_qstats_get_list *qstats;
        struct netdev_qstats_get_rsp **sorted;
        unsigned int count = 0;
        unsigned int i, j;
        int ret = 0;

        if (argc > 0) {
                p_err("balance command takes no arguments");
                return -1;
        }

        qstats = qstats_dump(NETDEV_QSTATS_SCOPE_QUEUE);
        if (!qstats)
                return -1;

        /* Count and sort queues */
        ynl_dump_foreach(qstats, qs)
                count++;

        if (count == 0) {
                if (json_output)
                        jsonw_start_array(json_wtr);
                else
                        printf("No queue statistics available\n");
                goto exit_free_qstats;
        }

        sorted = calloc(count, sizeof(*sorted));
        if (!sorted) {
                p_err("failed to allocate sorted array");
                ret = -1;
                goto exit_free_qstats;
        }

        i = 0;
        ynl_dump_foreach(qstats, qs)
                sorted[i++] = qs;

        qsort(sorted, count, sizeof(*sorted), cmp_ifindex_type);

        if (json_output)
                jsonw_start_array(json_wtr);

        /* Process each device/queue-type combination */
        i = 0;
        while (i < count) {
                __u64 *rx_packets, *rx_bytes, *tx_packets, *tx_bytes;
                enum netdev_queue_type type = sorted[i]->queue_type;
                unsigned int ifindex = sorted[i]->ifindex;
                unsigned int queue_count = 0;
                char ifname[IF_NAMESIZE];
                const char *name;

                /* Count queues for this device/type */
                for (j = i; j < count && sorted[j]->ifindex == ifindex &&
                     sorted[j]->queue_type == type; j++)
                        queue_count++;

                /* Skip if no packets/bytes (inactive queues) */
                if (!sorted[i]->_present.rx_packets &&
                    !sorted[i]->_present.rx_bytes &&
                    !sorted[i]->_present.tx_packets &&
                    !sorted[i]->_present.tx_bytes)
                        goto next_ifc;

                /* Allocate arrays for statistics */
                rx_packets = calloc(queue_count, sizeof(*rx_packets));
                rx_bytes   = calloc(queue_count, sizeof(*rx_bytes));
                tx_packets = calloc(queue_count, sizeof(*tx_packets));
                tx_bytes   = calloc(queue_count, sizeof(*tx_bytes));

                if (!rx_packets || !rx_bytes || !tx_packets || !tx_bytes) {
                        p_err("failed to allocate statistics arrays");
                        free(rx_packets);
                        free(rx_bytes);
                        free(tx_packets);
                        free(tx_bytes);
                        ret = -1;
                        goto exit_free_sorted;
                }

                /* Collect statistics */
                for (j = 0; j < queue_count; j++) {
                        rx_packets[j] = sorted[i + j]->_present.rx_packets ?
                                        sorted[i + j]->rx_packets : 0;
                        rx_bytes[j] = sorted[i + j]->_present.rx_bytes ?
                                      sorted[i + j]->rx_bytes : 0;
                        tx_packets[j] = sorted[i + j]->_present.tx_packets ?
                                        sorted[i + j]->tx_packets : 0;
                        tx_bytes[j] = sorted[i + j]->_present.tx_bytes ?
                                      sorted[i + j]->tx_bytes : 0;
                }

                name = if_indextoname(ifindex, ifname);

                if (json_output) {
                        jsonw_start_object(json_wtr);
                        if (name)
                                jsonw_string_field(json_wtr, "ifname", name);
                        jsonw_uint_field(json_wtr, "ifindex", ifindex);
                        jsonw_string_field(json_wtr, "queue-type",
                                           netdev_queue_type_str(type));

                        print_balance_stats_json("rx-packets", type,
                                                 rx_packets, queue_count);
                        print_balance_stats_json("rx-bytes", type,
                                                 rx_bytes, queue_count);
                        print_balance_stats_json("tx-packets", type,
                                                 tx_packets, queue_count);
                        print_balance_stats_json("tx-bytes", type,
                                                 tx_bytes, queue_count);

                        jsonw_end_object(json_wtr);
                } else {
                        if (name)
                                printf("%s", name);
                        else
                                printf("ifindex:%u", ifindex);
                        printf(" %s %d queues:\n",
                               netdev_queue_type_str(type), queue_count);

                        print_balance_stats("rx-packets", type,
                                            rx_packets, queue_count);
                        print_balance_stats("rx-bytes", type,
                                            rx_bytes, queue_count);
                        print_balance_stats("tx-packets", type,
                                            tx_packets, queue_count);
                        print_balance_stats("tx-bytes", type,
                                            tx_bytes, queue_count);
                        printf("\n");
                }

                free(rx_packets);
                free(rx_bytes);
                free(tx_packets);
                free(tx_bytes);

next_ifc:
                i += queue_count;
        }

        if (json_output)
                jsonw_end_array(json_wtr);

exit_free_sorted:
        free(sorted);
exit_free_qstats:
        netdev_qstats_get_list_free(qstats);
        return ret;
}

static int do_hw_gro(int argc, char **argv __attribute__((unused)))
{
        struct netdev_qstats_get_list *qstats;

        if (argc > 0) {
                p_err("hw-gro command takes no arguments");
                return -1;
        }

        qstats = qstats_dump(0);
        if (!qstats)
                return -1;

        if (json_output)
                jsonw_start_array(json_wtr);

        ynl_dump_foreach(qstats, qs) {
                char ifname[IF_NAMESIZE];
                const char *name;
                double savings;

                if (!qs->_present.rx_packets ||
                    !qs->_present.rx_hw_gro_packets ||
                    !qs->_present.rx_hw_gro_wire_packets)
                        continue;

                if (!qs->rx_packets)
                        continue;

                /* How many skbs did we avoid allocating thanks to HW GRO */
                savings = (double)(qs->rx_hw_gro_wire_packets -
                                   qs->rx_hw_gro_packets) /
                        qs->rx_packets * 100.0;

                name = if_indextoname(qs->ifindex, ifname);

                if (json_output) {
                        jsonw_start_object(json_wtr);
                        jsonw_uint_field(json_wtr, "ifindex", qs->ifindex);
                        if (name)
                                jsonw_string_field(json_wtr, "ifname", name);
                        jsonw_float_field(json_wtr, "savings", savings);
                        jsonw_end_object(json_wtr);
                } else {
                        if (name)
                                printf("%s", name);
                        else
                                printf("ifindex:%u", qs->ifindex);
                        printf(": %.1f%% savings\n", savings);
                }
        }

        if (json_output)
                jsonw_end_array(json_wtr);

        netdev_qstats_get_list_free(qstats);
        return 0;
}

static int do_help(int argc __attribute__((unused)),
                   char **argv __attribute__((unused)))
{
        if (json_output) {
                jsonw_null(json_wtr);
                return 0;
        }

        fprintf(stderr,
                "Usage: %1$s qstats { COMMAND | help }\n"
                "       %1$s qstats [ show ] [ OPTIONS ]\n"
                "       %1$s qstats balance\n"
                "       %1$s qstats hw-gro\n"
                "\n"
                "       OPTIONS := { scope queue | group-by { device | queue } }\n"
                "\n"
                "       show                  - Display queue statistics (default)\n"
                "                               Statistics are aggregated for the entire device.\n"
                "       show scope queue      - Display per-queue statistics\n"
                "       show group-by device  - Display device-aggregated statistics (default)\n"
                "       show group-by queue   - Display per-queue statistics\n"
                "\n"
                "  Analysis:\n"
                "       balance               - Traffic distribution between queues.\n"
                "       hw-gro                - HW GRO effectiveness analysis\n"
                "                               - savings - delta between packets received\n"
                "                                 on the wire and packets seen by the kernel.\n"
                "",
                bin_name);

        return 0;
}

static const struct cmd qstats_cmds[] = {
        { "show",       do_show },
        { "balance",    do_balance },
        { "hw-gro",     do_hw_gro },
        { "help",       do_help },
        { 0 }
};

int do_qstats(int argc, char **argv)
{
        return cmd_select(qstats_cmds, argc, argv, do_help);
}