* [PATCH net-next 1/5] netlink: specs: netdev add missing stats to qstat-get
2025-11-04 23:23 [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
@ 2025-11-04 23:23 ` Jakub Kicinski
2025-11-04 23:23 ` [PATCH net-next 2/5] tools: ynltool: create skeleton for the C command Jakub Kicinski
` (5 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Jakub Kicinski @ 2025-11-04 23:23 UTC (permalink / raw)
To: davem, donald.hunter
Cc: netdev, edumazet, pabeni, andrew+netdev, horms, sdf, joe,
jstancek, Jakub Kicinski
Add missing entries in C attribute list.
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
Documentation/netlink/specs/netdev.yaml | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/Documentation/netlink/specs/netdev.yaml b/Documentation/netlink/specs/netdev.yaml
index 10c412b7433f..82bf5cb2617d 100644
--- a/Documentation/netlink/specs/netdev.yaml
+++ b/Documentation/netlink/specs/netdev.yaml
@@ -733,6 +733,29 @@ doc: >-
- rx-bytes
- tx-packets
- tx-bytes
+ - rx-alloc-fail
+ - rx-hw-drops
+ - rx-hw-drop-overruns
+ - rx-csum-complete
+ - rx-csum-unnecessary
+ - rx-csum-none
+ - rx-csum-bad
+ - rx-hw-gro-packets
+ - rx-hw-gro-bytes
+ - rx-hw-gro-wire-packets
+ - rx-hw-gro-wire-bytes
+ - rx-hw-drop-ratelimits
+ - tx-hw-drops
+ - tx-hw-drop-errors
+ - tx-csum-none
+ - tx-needs-csum
+ - tx-hw-gso-packets
+ - tx-hw-gso-bytes
+ - tx-hw-gso-wire-packets
+ - tx-hw-gso-wire-bytes
+ - tx-hw-drop-ratelimits
+ - tx-stop
+ - tx-wake
-
name: bind-rx
doc: Bind dmabuf to netdev
--
2.51.1
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net-next 2/5] tools: ynltool: create skeleton for the C command
2025-11-04 23:23 [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
2025-11-04 23:23 ` [PATCH net-next 1/5] netlink: specs: netdev add missing stats to qstat-get Jakub Kicinski
@ 2025-11-04 23:23 ` Jakub Kicinski
2025-11-06 16:37 ` Stanislav Fomichev
2025-11-04 23:23 ` [PATCH net-next 3/5] tools: ynltool: add page-pool stats Jakub Kicinski
` (4 subsequent siblings)
6 siblings, 1 reply; 11+ messages in thread
From: Jakub Kicinski @ 2025-11-04 23:23 UTC (permalink / raw)
To: davem, donald.hunter
Cc: netdev, edumazet, pabeni, andrew+netdev, horms, sdf, joe,
jstancek, Jakub Kicinski
Based on past discussions it seems like integration of YNL into
iproute2 is unlikely. YNL itself is not great as a C library,
since it has no backward compat (we routinely change types).
Most of the operations can be performed with the generic Python
CLI directly. There is, however, a handful of operations where
summarization of kernel output is very useful (mostly related
to stats: page-pool, qstat).
Create a command (inspired by bpftool, I think it stood the test
of time reasonably well) to be able to plug the subcommands into.
Link: https://lore.kernel.org/1754895902-8790-1-git-send-email-ernis@linux.microsoft.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
tools/net/ynl/Makefile | 3 +-
tools/net/ynl/ynltool/Makefile | 44 +++++
tools/net/ynl/ynltool/json_writer.h | 75 ++++++++
tools/net/ynl/ynltool/main.h | 62 ++++++
tools/net/ynl/ynltool/json_writer.c | 288 ++++++++++++++++++++++++++++
tools/net/ynl/ynltool/main.c | 240 +++++++++++++++++++++++
tools/net/ynl/ynltool/.gitignore | 1 +
7 files changed, 712 insertions(+), 1 deletion(-)
create mode 100644 tools/net/ynl/ynltool/Makefile
create mode 100644 tools/net/ynl/ynltool/json_writer.h
create mode 100644 tools/net/ynl/ynltool/main.h
create mode 100644 tools/net/ynl/ynltool/json_writer.c
create mode 100644 tools/net/ynl/ynltool/main.c
create mode 100644 tools/net/ynl/ynltool/.gitignore
diff --git a/tools/net/ynl/Makefile b/tools/net/ynl/Makefile
index 211df5a93ad9..31ed20c0f3f8 100644
--- a/tools/net/ynl/Makefile
+++ b/tools/net/ynl/Makefile
@@ -12,10 +12,11 @@ endif
libdir ?= $(prefix)/$(libdir_relative)
includedir ?= $(prefix)/include
-SUBDIRS = lib generated samples
+SUBDIRS = lib generated samples ynltool
all: $(SUBDIRS) libynl.a
+ynltool: | lib generated libynl.a
samples: | lib generated
libynl.a: | lib generated
@echo -e "\tAR $@"
diff --git a/tools/net/ynl/ynltool/Makefile b/tools/net/ynl/ynltool/Makefile
new file mode 100644
index 000000000000..ce27dc691ffe
--- /dev/null
+++ b/tools/net/ynl/ynltool/Makefile
@@ -0,0 +1,44 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+include ../Makefile.deps
+
+INSTALL ?= install
+prefix ?= /usr
+
+CC := gcc
+CFLAGS := -Wall -Wextra -Werror -O2
+ifeq ("$(DEBUG)","1")
+ CFLAGS += -g -fsanitize=address -fsanitize=leak -static-libasan
+endif
+CFLAGS += -I../lib
+
+SRCS := $(wildcard *.c)
+OBJS := $(patsubst %.c,$(OUTPUT)%.o,$(SRCS))
+
+YNLTOOL := $(OUTPUT)ynltool
+
+include $(wildcard *.d)
+
+all: $(YNLTOOL)
+
+$(YNLTOOL): $(OBJS)
+ @echo -e "\tLINK $@"
+ @$(CC) $(CFLAGS) -o $@ $(OBJS)
+
+%.o: %.c main.h json_writer.h
+ @echo -e "\tCC $@"
+ @$(COMPILE.c) -MMD -c -o $@ $<
+
+clean:
+ rm -f *.o *.d *~
+
+distclean: clean
+ rm -f $(YNLTOOL)
+
+bindir ?= /usr/bin
+
+install: $(YNLTOOL)
+ install -m 0755 $(YNLTOOL) $(DESTDIR)$(bindir)/$(YNLTOOL)
+
+.PHONY: all clean distclean
+.DEFAULT_GOAL=all
diff --git a/tools/net/ynl/ynltool/json_writer.h b/tools/net/ynl/ynltool/json_writer.h
new file mode 100644
index 000000000000..0f1e63c88f6a
--- /dev/null
+++ b/tools/net/ynl/ynltool/json_writer.h
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Simple streaming JSON writer
+ *
+ * This takes care of the annoying bits of JSON syntax like the commas
+ * after elements
+ *
+ * Authors: Stephen Hemminger <stephen@networkplumber.org>
+ */
+
+#ifndef _JSON_WRITER_H_
+#define _JSON_WRITER_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdarg.h>
+#include <stdio.h>
+
+/* Opaque class structure */
+typedef struct json_writer json_writer_t;
+
+/* Create a new JSON stream */
+json_writer_t *jsonw_new(FILE *f);
+/* End output to JSON stream */
+void jsonw_destroy(json_writer_t **self_p);
+
+/* Cause output to have pretty whitespace */
+void jsonw_pretty(json_writer_t *self, bool on);
+
+/* Reset separator to create new JSON */
+void jsonw_reset(json_writer_t *self);
+
+/* Add property name */
+void jsonw_name(json_writer_t *self, const char *name);
+
+/* Add value */
+void __attribute__((format(printf, 2, 0))) jsonw_vprintf_enquote(json_writer_t *self,
+ const char *fmt,
+ va_list ap);
+void __attribute__((format(printf, 2, 3))) jsonw_printf(json_writer_t *self,
+ const char *fmt, ...);
+void jsonw_string(json_writer_t *self, const char *value);
+void jsonw_bool(json_writer_t *self, bool value);
+void jsonw_float(json_writer_t *self, double number);
+void jsonw_float_fmt(json_writer_t *self, const char *fmt, double num);
+void jsonw_uint(json_writer_t *self, uint64_t number);
+void jsonw_hu(json_writer_t *self, unsigned short number);
+void jsonw_int(json_writer_t *self, int64_t number);
+void jsonw_null(json_writer_t *self);
+void jsonw_lluint(json_writer_t *self, unsigned long long int num);
+
+/* Useful Combinations of name and value */
+void jsonw_string_field(json_writer_t *self, const char *prop, const char *val);
+void jsonw_bool_field(json_writer_t *self, const char *prop, bool value);
+void jsonw_float_field(json_writer_t *self, const char *prop, double num);
+void jsonw_uint_field(json_writer_t *self, const char *prop, uint64_t num);
+void jsonw_hu_field(json_writer_t *self, const char *prop, unsigned short num);
+void jsonw_int_field(json_writer_t *self, const char *prop, int64_t num);
+void jsonw_null_field(json_writer_t *self, const char *prop);
+void jsonw_lluint_field(json_writer_t *self, const char *prop,
+ unsigned long long int num);
+void jsonw_float_field_fmt(json_writer_t *self, const char *prop,
+ const char *fmt, double val);
+
+/* Collections */
+void jsonw_start_object(json_writer_t *self);
+void jsonw_end_object(json_writer_t *self);
+
+void jsonw_start_array(json_writer_t *self);
+void jsonw_end_array(json_writer_t *self);
+
+/* Override default exception handling */
+typedef void (jsonw_err_handler_fn)(const char *);
+
+#endif /* _JSON_WRITER_H_ */
diff --git a/tools/net/ynl/ynltool/main.h b/tools/net/ynl/ynltool/main.h
new file mode 100644
index 000000000000..f4a70acf2085
--- /dev/null
+++ b/tools/net/ynl/ynltool/main.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
+/* Copyright Meta Platforms, Inc. and affiliates */
+
+#ifndef __YNLTOOL_H
+#define __YNLTOOL_H
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+#include "json_writer.h"
+
+#define NEXT_ARG() ({ argc--; argv++; if (argc < 0) usage(); })
+#define NEXT_ARGP() ({ (*argc)--; (*argv)++; if (*argc < 0) usage(); })
+#define BAD_ARG() ({ p_err("what is '%s'?", *argv); -1; })
+#define GET_ARG() ({ argc--; *argv++; })
+#define REQ_ARGS(cnt) \
+ ({ \
+ int _cnt = (cnt); \
+ bool _res; \
+ \
+ if (argc < _cnt) { \
+ p_err("'%s' needs at least %d arguments, %d found", \
+ argv[-1], _cnt, argc); \
+ _res = false; \
+ } else { \
+ _res = true; \
+ } \
+ _res; \
+ })
+
+#define HELP_SPEC_OPTIONS \
+ "OPTIONS := { {-j|--json} [{-p|--pretty}] }"
+
+extern const char *bin_name;
+
+extern json_writer_t *json_wtr;
+extern bool json_output;
+extern bool pretty_output;
+
+void __attribute__((format(printf, 1, 2))) p_err(const char *fmt, ...);
+void __attribute__((format(printf, 1, 2))) p_info(const char *fmt, ...);
+
+bool is_prefix(const char *pfx, const char *str);
+int detect_common_prefix(const char *arg, ...);
+void usage(void) __attribute__((noreturn));
+
+struct cmd {
+ const char *cmd;
+ int (*func)(int argc, char **argv);
+};
+
+int cmd_select(const struct cmd *cmds, int argc, char **argv,
+ int (*help)(int argc, char **argv));
+
+#endif /* __YNLTOOL_H */
diff --git a/tools/net/ynl/ynltool/json_writer.c b/tools/net/ynl/ynltool/json_writer.c
new file mode 100644
index 000000000000..c8685e592cd3
--- /dev/null
+++ b/tools/net/ynl/ynltool/json_writer.c
@@ -0,0 +1,288 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * Simple streaming JSON writer
+ *
+ * This takes care of the annoying bits of JSON syntax like the commas
+ * after elements
+ *
+ * Authors: Stephen Hemminger <stephen@networkplumber.org>
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <malloc.h>
+#include <inttypes.h>
+#include <stdint.h>
+
+#include "json_writer.h"
+
+struct json_writer {
+ FILE *out;
+ unsigned depth;
+ bool pretty;
+ char sep;
+};
+
+static void jsonw_indent(json_writer_t *self)
+{
+ unsigned i;
+ for (i = 0; i < self->depth; ++i)
+ fputs(" ", self->out);
+}
+
+static void jsonw_eol(json_writer_t *self)
+{
+ if (!self->pretty)
+ return;
+
+ putc('\n', self->out);
+ jsonw_indent(self);
+}
+
+static void jsonw_eor(json_writer_t *self)
+{
+ if (self->sep != '\0')
+ putc(self->sep, self->out);
+ self->sep = ',';
+}
+
+static void jsonw_puts(json_writer_t *self, const char *str)
+{
+ putc('"', self->out);
+ for (; *str; ++str)
+ switch (*str) {
+ case '\t':
+ fputs("\\t", self->out);
+ break;
+ case '\n':
+ fputs("\\n", self->out);
+ break;
+ case '\r':
+ fputs("\\r", self->out);
+ break;
+ case '\f':
+ fputs("\\f", self->out);
+ break;
+ case '\b':
+ fputs("\\b", self->out);
+ break;
+ case '\\':
+ fputs("\\\\", self->out);
+ break;
+ case '"':
+ fputs("\\\"", self->out);
+ break;
+ default:
+ putc(*str, self->out);
+ }
+ putc('"', self->out);
+}
+
+json_writer_t *jsonw_new(FILE *f)
+{
+ json_writer_t *self = malloc(sizeof(*self));
+ if (self) {
+ self->out = f;
+ self->depth = 0;
+ self->pretty = false;
+ self->sep = '\0';
+ }
+ return self;
+}
+
+void jsonw_destroy(json_writer_t **self_p)
+{
+ json_writer_t *self = *self_p;
+
+ assert(self->depth == 0);
+ fputs("\n", self->out);
+ fflush(self->out);
+ free(self);
+ *self_p = NULL;
+}
+
+void jsonw_pretty(json_writer_t *self, bool on)
+{
+ self->pretty = on;
+}
+
+void jsonw_reset(json_writer_t *self)
+{
+ assert(self->depth == 0);
+ self->sep = '\0';
+}
+
+static void jsonw_begin(json_writer_t *self, int c)
+{
+ jsonw_eor(self);
+ putc(c, self->out);
+ ++self->depth;
+ self->sep = '\0';
+}
+
+static void jsonw_end(json_writer_t *self, int c)
+{
+ assert(self->depth > 0);
+
+ --self->depth;
+ if (self->sep != '\0')
+ jsonw_eol(self);
+ putc(c, self->out);
+ self->sep = ',';
+}
+
+void jsonw_name(json_writer_t *self, const char *name)
+{
+ jsonw_eor(self);
+ jsonw_eol(self);
+ self->sep = '\0';
+ jsonw_puts(self, name);
+ putc(':', self->out);
+ if (self->pretty)
+ putc(' ', self->out);
+}
+
+void jsonw_vprintf_enquote(json_writer_t *self, const char *fmt, va_list ap)
+{
+ jsonw_eor(self);
+ putc('"', self->out);
+ vfprintf(self->out, fmt, ap);
+ putc('"', self->out);
+}
+
+void jsonw_printf(json_writer_t *self, const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ jsonw_eor(self);
+ vfprintf(self->out, fmt, ap);
+ va_end(ap);
+}
+
+void jsonw_start_object(json_writer_t *self)
+{
+ jsonw_begin(self, '{');
+}
+
+void jsonw_end_object(json_writer_t *self)
+{
+ jsonw_end(self, '}');
+}
+
+void jsonw_start_array(json_writer_t *self)
+{
+ jsonw_begin(self, '[');
+}
+
+void jsonw_end_array(json_writer_t *self)
+{
+ jsonw_end(self, ']');
+}
+
+void jsonw_string(json_writer_t *self, const char *value)
+{
+ jsonw_eor(self);
+ jsonw_puts(self, value);
+}
+
+void jsonw_bool(json_writer_t *self, bool val)
+{
+ jsonw_printf(self, "%s", val ? "true" : "false");
+}
+
+void jsonw_null(json_writer_t *self)
+{
+ jsonw_printf(self, "null");
+}
+
+void jsonw_float_fmt(json_writer_t *self, const char *fmt, double num)
+{
+ jsonw_printf(self, fmt, num);
+}
+
+void jsonw_float(json_writer_t *self, double num)
+{
+ jsonw_printf(self, "%g", num);
+}
+
+void jsonw_hu(json_writer_t *self, unsigned short num)
+{
+ jsonw_printf(self, "%hu", num);
+}
+
+void jsonw_uint(json_writer_t *self, uint64_t num)
+{
+ jsonw_printf(self, "%"PRIu64, num);
+}
+
+void jsonw_lluint(json_writer_t *self, unsigned long long int num)
+{
+ jsonw_printf(self, "%llu", num);
+}
+
+void jsonw_int(json_writer_t *self, int64_t num)
+{
+ jsonw_printf(self, "%"PRId64, num);
+}
+
+void jsonw_string_field(json_writer_t *self, const char *prop, const char *val)
+{
+ jsonw_name(self, prop);
+ jsonw_string(self, val);
+}
+
+void jsonw_bool_field(json_writer_t *self, const char *prop, bool val)
+{
+ jsonw_name(self, prop);
+ jsonw_bool(self, val);
+}
+
+void jsonw_float_field(json_writer_t *self, const char *prop, double val)
+{
+ jsonw_name(self, prop);
+ jsonw_float(self, val);
+}
+
+void jsonw_float_field_fmt(json_writer_t *self,
+ const char *prop,
+ const char *fmt,
+ double val)
+{
+ jsonw_name(self, prop);
+ jsonw_float_fmt(self, fmt, val);
+}
+
+void jsonw_uint_field(json_writer_t *self, const char *prop, uint64_t num)
+{
+ jsonw_name(self, prop);
+ jsonw_uint(self, num);
+}
+
+void jsonw_hu_field(json_writer_t *self, const char *prop, unsigned short num)
+{
+ jsonw_name(self, prop);
+ jsonw_hu(self, num);
+}
+
+void jsonw_lluint_field(json_writer_t *self,
+ const char *prop,
+ unsigned long long int num)
+{
+ jsonw_name(self, prop);
+ jsonw_lluint(self, num);
+}
+
+void jsonw_int_field(json_writer_t *self, const char *prop, int64_t num)
+{
+ jsonw_name(self, prop);
+ jsonw_int(self, num);
+}
+
+void jsonw_null_field(json_writer_t *self, const char *prop)
+{
+ jsonw_name(self, prop);
+ jsonw_null(self);
+}
diff --git a/tools/net/ynl/ynltool/main.c b/tools/net/ynl/ynltool/main.c
new file mode 100644
index 000000000000..c5047fad50cf
--- /dev/null
+++ b/tools/net/ynl/ynltool/main.c
@@ -0,0 +1,240 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
+/* Copyright Meta Platforms, Inc. and affiliates */
+
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+
+#include "main.h"
+
+const char *bin_name;
+static int last_argc;
+static char **last_argv;
+static int (*last_do_help)(int argc, char **argv);
+json_writer_t *json_wtr;
+bool pretty_output;
+bool json_output;
+
+static void __attribute__((noreturn)) clean_and_exit(int i)
+{
+ if (json_output)
+ jsonw_destroy(&json_wtr);
+
+ exit(i);
+}
+
+void usage(void)
+{
+ last_do_help(last_argc - 1, last_argv + 1);
+
+ clean_and_exit(-1);
+}
+
+static int do_help(int argc __attribute__((unused)),
+ char **argv __attribute__((unused)))
+{
+ if (json_output) {
+ jsonw_null(json_wtr);
+ return 0;
+ }
+
+ fprintf(stderr,
+ "Usage: %s [OPTIONS] OBJECT { COMMAND | help }\n"
+ " %s version\n"
+ "\n"
+ " OBJECT := { }\n"
+ " " HELP_SPEC_OPTIONS "\n"
+ "",
+ bin_name, bin_name);
+
+ return 0;
+}
+
+static int do_version(int argc __attribute__((unused)),
+ char **argv __attribute__((unused)))
+{
+ if (json_output) {
+ jsonw_start_object(json_wtr);
+ jsonw_name(json_wtr, "version");
+ jsonw_printf(json_wtr, "\"0.1.0\"");
+ jsonw_end_object(json_wtr);
+ } else {
+ printf("%s v0.1.0\n", bin_name);
+ }
+ return 0;
+}
+
+static const struct cmd commands[] = {
+ { "help", do_help },
+ { "version", do_version },
+ { 0 }
+};
+
+int cmd_select(const struct cmd *cmds, int argc, char **argv,
+ int (*help)(int argc, char **argv))
+{
+ unsigned int i;
+
+ last_argc = argc;
+ last_argv = argv;
+ last_do_help = help;
+
+ if (argc < 1 && cmds[0].func)
+ return cmds[0].func(argc, argv);
+
+ for (i = 0; cmds[i].cmd; i++) {
+ if (is_prefix(*argv, cmds[i].cmd)) {
+ if (!cmds[i].func) {
+ p_err("command '%s' is not available", cmds[i].cmd);
+ return -1;
+ }
+ return cmds[i].func(argc - 1, argv + 1);
+ }
+ }
+
+ help(argc - 1, argv + 1);
+
+ return -1;
+}
+
+bool is_prefix(const char *pfx, const char *str)
+{
+ if (!pfx)
+ return false;
+ if (strlen(str) < strlen(pfx))
+ return false;
+
+ return !memcmp(str, pfx, strlen(pfx));
+}
+
+/* Last argument MUST be NULL pointer */
+int detect_common_prefix(const char *arg, ...)
+{
+ unsigned int count = 0;
+ const char *ref;
+ char msg[256];
+ va_list ap;
+
+ snprintf(msg, sizeof(msg), "ambiguous prefix: '%s' could be '", arg);
+ va_start(ap, arg);
+ while ((ref = va_arg(ap, const char *))) {
+ if (!is_prefix(arg, ref))
+ continue;
+ count++;
+ if (count > 1)
+ strncat(msg, "' or '", sizeof(msg) - strlen(msg) - 1);
+ strncat(msg, ref, sizeof(msg) - strlen(msg) - 1);
+ }
+ va_end(ap);
+ strncat(msg, "'", sizeof(msg) - strlen(msg) - 1);
+
+ if (count >= 2) {
+ p_err("%s", msg);
+ return -1;
+ }
+
+ return 0;
+}
+
+void p_err(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ if (json_output) {
+ jsonw_start_object(json_wtr);
+ jsonw_name(json_wtr, "error");
+ jsonw_vprintf_enquote(json_wtr, fmt, ap);
+ jsonw_end_object(json_wtr);
+ } else {
+ fprintf(stderr, "Error: ");
+ vfprintf(stderr, fmt, ap);
+ fprintf(stderr, "\n");
+ }
+ va_end(ap);
+}
+
+void p_info(const char *fmt, ...)
+{
+ va_list ap;
+
+ if (json_output)
+ return;
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ fprintf(stderr, "\n");
+ va_end(ap);
+}
+
+int main(int argc, char **argv)
+{
+ static const struct option options[] = {
+ { "json", no_argument, NULL, 'j' },
+ { "help", no_argument, NULL, 'h' },
+ { "pretty", no_argument, NULL, 'p' },
+ { "version", no_argument, NULL, 'V' },
+ { 0 }
+ };
+ bool version_requested = false;
+ int opt, ret;
+
+ setlinebuf(stdout);
+
+ last_do_help = do_help;
+ pretty_output = false;
+ json_output = false;
+ bin_name = "ynltool";
+
+ opterr = 0;
+ while ((opt = getopt_long(argc, argv, "Vhjp",
+ options, NULL)) >= 0) {
+ switch (opt) {
+ case 'V':
+ version_requested = true;
+ break;
+ case 'h':
+ return do_help(argc, argv);
+ case 'p':
+ pretty_output = true;
+ /* fall through */
+ case 'j':
+ if (!json_output) {
+ json_wtr = jsonw_new(stdout);
+ if (!json_wtr) {
+ p_err("failed to create JSON writer");
+ return -1;
+ }
+ json_output = true;
+ }
+ jsonw_pretty(json_wtr, pretty_output);
+ break;
+ default:
+ p_err("unrecognized option '%s'", argv[optind - 1]);
+ if (json_output)
+ clean_and_exit(-1);
+ else
+ usage();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+ if (argc < 0)
+ usage();
+
+ if (version_requested)
+ ret = do_version(argc, argv);
+ else
+ ret = cmd_select(commands, argc, argv, do_help);
+
+ if (json_output)
+ jsonw_destroy(&json_wtr);
+
+ return ret;
+}
diff --git a/tools/net/ynl/ynltool/.gitignore b/tools/net/ynl/ynltool/.gitignore
new file mode 100644
index 000000000000..f38848dbb0d3
--- /dev/null
+++ b/tools/net/ynl/ynltool/.gitignore
@@ -0,0 +1 @@
+ynltool
--
2.51.1
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH net-next 2/5] tools: ynltool: create skeleton for the C command
2025-11-04 23:23 ` [PATCH net-next 2/5] tools: ynltool: create skeleton for the C command Jakub Kicinski
@ 2025-11-06 16:37 ` Stanislav Fomichev
2025-11-06 22:24 ` Jakub Kicinski
0 siblings, 1 reply; 11+ messages in thread
From: Stanislav Fomichev @ 2025-11-06 16:37 UTC (permalink / raw)
To: Jakub Kicinski
Cc: davem, donald.hunter, netdev, edumazet, pabeni, andrew+netdev,
horms, sdf, joe, jstancek
On 11/04, Jakub Kicinski wrote:
> Based on past discussions it seems like integration of YNL into
> iproute2 is unlikely. YNL itself is not great as a C library,
> since it has no backward compat (we routinely change types).
>
> Most of the operations can be performed with the generic Python
> CLI directly. There is, however, a handful of operations where
> summarization of kernel output is very useful (mostly related
> to stats: page-pool, qstat).
>
> Create a command (inspired by bpftool, I think it stood the test
> of time reasonably well) to be able to plug the subcommands into.
>
> Link: https://lore.kernel.org/1754895902-8790-1-git-send-email-ernis@linux.microsoft.com
> Signed-off-by: Jakub Kicinski <kuba@kernel.org>
> ---
> tools/net/ynl/Makefile | 3 +-
> tools/net/ynl/ynltool/Makefile | 44 +++++
> tools/net/ynl/ynltool/json_writer.h | 75 ++++++++
> tools/net/ynl/ynltool/main.h | 62 ++++++
> tools/net/ynl/ynltool/json_writer.c | 288 ++++++++++++++++++++++++++++
> tools/net/ynl/ynltool/main.c | 240 +++++++++++++++++++++++
> tools/net/ynl/ynltool/.gitignore | 1 +
> 7 files changed, 712 insertions(+), 1 deletion(-)
> create mode 100644 tools/net/ynl/ynltool/Makefile
> create mode 100644 tools/net/ynl/ynltool/json_writer.h
> create mode 100644 tools/net/ynl/ynltool/main.h
> create mode 100644 tools/net/ynl/ynltool/json_writer.c
> create mode 100644 tools/net/ynl/ynltool/main.c
> create mode 100644 tools/net/ynl/ynltool/.gitignore
>
> diff --git a/tools/net/ynl/Makefile b/tools/net/ynl/Makefile
> index 211df5a93ad9..31ed20c0f3f8 100644
> --- a/tools/net/ynl/Makefile
> +++ b/tools/net/ynl/Makefile
> @@ -12,10 +12,11 @@ endif
> libdir ?= $(prefix)/$(libdir_relative)
> includedir ?= $(prefix)/include
>
> -SUBDIRS = lib generated samples
> +SUBDIRS = lib generated samples ynltool
>
> all: $(SUBDIRS) libynl.a
>
> +ynltool: | lib generated libynl.a
> samples: | lib generated
> libynl.a: | lib generated
> @echo -e "\tAR $@"
> diff --git a/tools/net/ynl/ynltool/Makefile b/tools/net/ynl/ynltool/Makefile
> new file mode 100644
> index 000000000000..ce27dc691ffe
> --- /dev/null
> +++ b/tools/net/ynl/ynltool/Makefile
> @@ -0,0 +1,44 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +
> +include ../Makefile.deps
> +
> +INSTALL ?= install
> +prefix ?= /usr
> +
> +CC := gcc
> +CFLAGS := -Wall -Wextra -Werror -O2
> +ifeq ("$(DEBUG)","1")
> + CFLAGS += -g -fsanitize=address -fsanitize=leak -static-libasan
> +endif
> +CFLAGS += -I../lib
> +
> +SRCS := $(wildcard *.c)
> +OBJS := $(patsubst %.c,$(OUTPUT)%.o,$(SRCS))
> +
> +YNLTOOL := $(OUTPUT)ynltool
> +
> +include $(wildcard *.d)
> +
> +all: $(YNLTOOL)
> +
> +$(YNLTOOL): $(OBJS)
> + @echo -e "\tLINK $@"
> + @$(CC) $(CFLAGS) -o $@ $(OBJS)
> +
> +%.o: %.c main.h json_writer.h
> + @echo -e "\tCC $@"
> + @$(COMPILE.c) -MMD -c -o $@ $<
> +
> +clean:
> + rm -f *.o *.d *~
> +
> +distclean: clean
> + rm -f $(YNLTOOL)
> +
> +bindir ?= /usr/bin
> +
> +install: $(YNLTOOL)
> + install -m 0755 $(YNLTOOL) $(DESTDIR)$(bindir)/$(YNLTOOL)
> +
> +.PHONY: all clean distclean
> +.DEFAULT_GOAL=all
> diff --git a/tools/net/ynl/ynltool/json_writer.h b/tools/net/ynl/ynltool/json_writer.h
> new file mode 100644
> index 000000000000..0f1e63c88f6a
> --- /dev/null
> +++ b/tools/net/ynl/ynltool/json_writer.h
> @@ -0,0 +1,75 @@
> +/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
> +/*
> + * Simple streaming JSON writer
> + *
> + * This takes care of the annoying bits of JSON syntax like the commas
> + * after elements
> + *
> + * Authors: Stephen Hemminger <stephen@networkplumber.org>
> + */
> +
> +#ifndef _JSON_WRITER_H_
> +#define _JSON_WRITER_H_
> +
> +#include <stdbool.h>
> +#include <stdint.h>
> +#include <stdarg.h>
> +#include <stdio.h>
> +
> +/* Opaque class structure */
> +typedef struct json_writer json_writer_t;
> +
> +/* Create a new JSON stream */
> +json_writer_t *jsonw_new(FILE *f);
> +/* End output to JSON stream */
> +void jsonw_destroy(json_writer_t **self_p);
> +
> +/* Cause output to have pretty whitespace */
> +void jsonw_pretty(json_writer_t *self, bool on);
> +
> +/* Reset separator to create new JSON */
> +void jsonw_reset(json_writer_t *self);
> +
> +/* Add property name */
> +void jsonw_name(json_writer_t *self, const char *name);
> +
> +/* Add value */
> +void __attribute__((format(printf, 2, 0))) jsonw_vprintf_enquote(json_writer_t *self,
> + const char *fmt,
> + va_list ap);
> +void __attribute__((format(printf, 2, 3))) jsonw_printf(json_writer_t *self,
> + const char *fmt, ...);
> +void jsonw_string(json_writer_t *self, const char *value);
> +void jsonw_bool(json_writer_t *self, bool value);
> +void jsonw_float(json_writer_t *self, double number);
> +void jsonw_float_fmt(json_writer_t *self, const char *fmt, double num);
> +void jsonw_uint(json_writer_t *self, uint64_t number);
> +void jsonw_hu(json_writer_t *self, unsigned short number);
> +void jsonw_int(json_writer_t *self, int64_t number);
> +void jsonw_null(json_writer_t *self);
> +void jsonw_lluint(json_writer_t *self, unsigned long long int num);
> +
> +/* Useful Combinations of name and value */
> +void jsonw_string_field(json_writer_t *self, const char *prop, const char *val);
> +void jsonw_bool_field(json_writer_t *self, const char *prop, bool value);
> +void jsonw_float_field(json_writer_t *self, const char *prop, double num);
> +void jsonw_uint_field(json_writer_t *self, const char *prop, uint64_t num);
> +void jsonw_hu_field(json_writer_t *self, const char *prop, unsigned short num);
> +void jsonw_int_field(json_writer_t *self, const char *prop, int64_t num);
> +void jsonw_null_field(json_writer_t *self, const char *prop);
> +void jsonw_lluint_field(json_writer_t *self, const char *prop,
> + unsigned long long int num);
> +void jsonw_float_field_fmt(json_writer_t *self, const char *prop,
> + const char *fmt, double val);
> +
> +/* Collections */
> +void jsonw_start_object(json_writer_t *self);
> +void jsonw_end_object(json_writer_t *self);
> +
> +void jsonw_start_array(json_writer_t *self);
> +void jsonw_end_array(json_writer_t *self);
> +
> +/* Override default exception handling */
> +typedef void (jsonw_err_handler_fn)(const char *);
> +
> +#endif /* _JSON_WRITER_H_ */
> diff --git a/tools/net/ynl/ynltool/main.h b/tools/net/ynl/ynltool/main.h
> new file mode 100644
> index 000000000000..f4a70acf2085
> --- /dev/null
> +++ b/tools/net/ynl/ynltool/main.h
> @@ -0,0 +1,62 @@
> +/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
> +/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
> +/* Copyright Meta Platforms, Inc. and affiliates */
> +
> +#ifndef __YNLTOOL_H
> +#define __YNLTOOL_H
> +
> +#ifndef _GNU_SOURCE
> +#define _GNU_SOURCE
> +#endif
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <errno.h>
> +#include <string.h>
> +
> +#include "json_writer.h"
> +
> +#define NEXT_ARG() ({ argc--; argv++; if (argc < 0) usage(); })
> +#define NEXT_ARGP() ({ (*argc)--; (*argv)++; if (*argc < 0) usage(); })
> +#define BAD_ARG() ({ p_err("what is '%s'?", *argv); -1; })
> +#define GET_ARG() ({ argc--; *argv++; })
> +#define REQ_ARGS(cnt) \
> + ({ \
> + int _cnt = (cnt); \
> + bool _res; \
> + \
> + if (argc < _cnt) { \
> + p_err("'%s' needs at least %d arguments, %d found", \
> + argv[-1], _cnt, argc); \
> + _res = false; \
> + } else { \
> + _res = true; \
> + } \
> + _res; \
> + })
> +
> +#define HELP_SPEC_OPTIONS \
> + "OPTIONS := { {-j|--json} [{-p|--pretty}] }"
> +
> +extern const char *bin_name;
> +
> +extern json_writer_t *json_wtr;
> +extern bool json_output;
> +extern bool pretty_output;
> +
> +void __attribute__((format(printf, 1, 2))) p_err(const char *fmt, ...);
> +void __attribute__((format(printf, 1, 2))) p_info(const char *fmt, ...);
> +
> +bool is_prefix(const char *pfx, const char *str);
> +int detect_common_prefix(const char *arg, ...);
> +void usage(void) __attribute__((noreturn));
> +
> +struct cmd {
> + const char *cmd;
> + int (*func)(int argc, char **argv);
> +};
> +
> +int cmd_select(const struct cmd *cmds, int argc, char **argv,
> + int (*help)(int argc, char **argv));
> +
> +#endif /* __YNLTOOL_H */
> diff --git a/tools/net/ynl/ynltool/json_writer.c b/tools/net/ynl/ynltool/json_writer.c
> new file mode 100644
> index 000000000000..c8685e592cd3
> --- /dev/null
> +++ b/tools/net/ynl/ynltool/json_writer.c
> @@ -0,0 +1,288 @@
> +// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
> +/*
> + * Simple streaming JSON writer
> + *
> + * This takes care of the annoying bits of JSON syntax like the commas
> + * after elements
> + *
> + * Authors: Stephen Hemminger <stephen@networkplumber.org>
> + */
> +
> +#include <stdio.h>
> +#include <stdbool.h>
> +#include <stdarg.h>
> +#include <assert.h>
> +#include <malloc.h>
> +#include <inttypes.h>
> +#include <stdint.h>
> +
> +#include "json_writer.h"
> +
> +struct json_writer {
> + FILE *out;
> + unsigned depth;
> + bool pretty;
> + char sep;
> +};
> +
> +static void jsonw_indent(json_writer_t *self)
> +{
> + unsigned i;
> + for (i = 0; i < self->depth; ++i)
> + fputs(" ", self->out);
> +}
> +
> +static void jsonw_eol(json_writer_t *self)
> +{
> + if (!self->pretty)
> + return;
> +
> + putc('\n', self->out);
> + jsonw_indent(self);
> +}
> +
> +static void jsonw_eor(json_writer_t *self)
> +{
> + if (self->sep != '\0')
> + putc(self->sep, self->out);
> + self->sep = ',';
> +}
> +
> +static void jsonw_puts(json_writer_t *self, const char *str)
> +{
> + putc('"', self->out);
> + for (; *str; ++str)
> + switch (*str) {
> + case '\t':
> + fputs("\\t", self->out);
> + break;
> + case '\n':
> + fputs("\\n", self->out);
> + break;
> + case '\r':
> + fputs("\\r", self->out);
> + break;
> + case '\f':
> + fputs("\\f", self->out);
> + break;
> + case '\b':
> + fputs("\\b", self->out);
> + break;
> + case '\\':
> + fputs("\\\\", self->out);
> + break;
> + case '"':
> + fputs("\\\"", self->out);
> + break;
> + default:
> + putc(*str, self->out);
> + }
> + putc('"', self->out);
> +}
> +
> +json_writer_t *jsonw_new(FILE *f)
> +{
> + json_writer_t *self = malloc(sizeof(*self));
> + if (self) {
> + self->out = f;
> + self->depth = 0;
> + self->pretty = false;
> + self->sep = '\0';
> + }
> + return self;
> +}
> +
> +void jsonw_destroy(json_writer_t **self_p)
> +{
> + json_writer_t *self = *self_p;
> +
> + assert(self->depth == 0);
> + fputs("\n", self->out);
> + fflush(self->out);
> + free(self);
> + *self_p = NULL;
> +}
> +
> +void jsonw_pretty(json_writer_t *self, bool on)
> +{
> + self->pretty = on;
> +}
> +
> +void jsonw_reset(json_writer_t *self)
> +{
> + assert(self->depth == 0);
> + self->sep = '\0';
> +}
> +
> +static void jsonw_begin(json_writer_t *self, int c)
> +{
> + jsonw_eor(self);
> + putc(c, self->out);
> + ++self->depth;
> + self->sep = '\0';
> +}
> +
> +static void jsonw_end(json_writer_t *self, int c)
> +{
> + assert(self->depth > 0);
> +
> + --self->depth;
> + if (self->sep != '\0')
> + jsonw_eol(self);
> + putc(c, self->out);
> + self->sep = ',';
> +}
> +
> +void jsonw_name(json_writer_t *self, const char *name)
> +{
> + jsonw_eor(self);
> + jsonw_eol(self);
> + self->sep = '\0';
> + jsonw_puts(self, name);
> + putc(':', self->out);
> + if (self->pretty)
> + putc(' ', self->out);
> +}
> +
> +void jsonw_vprintf_enquote(json_writer_t *self, const char *fmt, va_list ap)
> +{
> + jsonw_eor(self);
> + putc('"', self->out);
> + vfprintf(self->out, fmt, ap);
> + putc('"', self->out);
> +}
> +
> +void jsonw_printf(json_writer_t *self, const char *fmt, ...)
> +{
> + va_list ap;
> +
> + va_start(ap, fmt);
> + jsonw_eor(self);
> + vfprintf(self->out, fmt, ap);
> + va_end(ap);
> +}
> +
> +void jsonw_start_object(json_writer_t *self)
> +{
> + jsonw_begin(self, '{');
> +}
> +
> +void jsonw_end_object(json_writer_t *self)
> +{
> + jsonw_end(self, '}');
> +}
> +
> +void jsonw_start_array(json_writer_t *self)
> +{
> + jsonw_begin(self, '[');
> +}
> +
> +void jsonw_end_array(json_writer_t *self)
> +{
> + jsonw_end(self, ']');
> +}
> +
> +void jsonw_string(json_writer_t *self, const char *value)
> +{
> + jsonw_eor(self);
> + jsonw_puts(self, value);
> +}
> +
> +void jsonw_bool(json_writer_t *self, bool val)
> +{
> + jsonw_printf(self, "%s", val ? "true" : "false");
> +}
> +
> +void jsonw_null(json_writer_t *self)
> +{
> + jsonw_printf(self, "null");
> +}
> +
> +void jsonw_float_fmt(json_writer_t *self, const char *fmt, double num)
> +{
> + jsonw_printf(self, fmt, num);
> +}
> +
> +void jsonw_float(json_writer_t *self, double num)
> +{
> + jsonw_printf(self, "%g", num);
> +}
> +
> +void jsonw_hu(json_writer_t *self, unsigned short num)
> +{
> + jsonw_printf(self, "%hu", num);
> +}
> +
> +void jsonw_uint(json_writer_t *self, uint64_t num)
> +{
> + jsonw_printf(self, "%"PRIu64, num);
> +}
> +
> +void jsonw_lluint(json_writer_t *self, unsigned long long int num)
> +{
> + jsonw_printf(self, "%llu", num);
> +}
> +
> +void jsonw_int(json_writer_t *self, int64_t num)
> +{
> + jsonw_printf(self, "%"PRId64, num);
> +}
> +
> +void jsonw_string_field(json_writer_t *self, const char *prop, const char *val)
> +{
> + jsonw_name(self, prop);
> + jsonw_string(self, val);
> +}
> +
> +void jsonw_bool_field(json_writer_t *self, const char *prop, bool val)
> +{
> + jsonw_name(self, prop);
> + jsonw_bool(self, val);
> +}
> +
> +void jsonw_float_field(json_writer_t *self, const char *prop, double val)
> +{
> + jsonw_name(self, prop);
> + jsonw_float(self, val);
> +}
> +
> +void jsonw_float_field_fmt(json_writer_t *self,
> + const char *prop,
> + const char *fmt,
> + double val)
> +{
> + jsonw_name(self, prop);
> + jsonw_float_fmt(self, fmt, val);
> +}
> +
> +void jsonw_uint_field(json_writer_t *self, const char *prop, uint64_t num)
> +{
> + jsonw_name(self, prop);
> + jsonw_uint(self, num);
> +}
> +
> +void jsonw_hu_field(json_writer_t *self, const char *prop, unsigned short num)
> +{
> + jsonw_name(self, prop);
> + jsonw_hu(self, num);
> +}
> +
> +void jsonw_lluint_field(json_writer_t *self,
> + const char *prop,
> + unsigned long long int num)
> +{
> + jsonw_name(self, prop);
> + jsonw_lluint(self, num);
> +}
> +
> +void jsonw_int_field(json_writer_t *self, const char *prop, int64_t num)
> +{
> + jsonw_name(self, prop);
> + jsonw_int(self, num);
> +}
> +
> +void jsonw_null_field(json_writer_t *self, const char *prop)
> +{
> + jsonw_name(self, prop);
> + jsonw_null(self);
> +}
> diff --git a/tools/net/ynl/ynltool/main.c b/tools/net/ynl/ynltool/main.c
> new file mode 100644
> index 000000000000..c5047fad50cf
> --- /dev/null
> +++ b/tools/net/ynl/ynltool/main.c
> @@ -0,0 +1,240 @@
> +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
> +/* Copyright Meta Platforms, Inc. and affiliates */
> +
> +#include <ctype.h>
> +#include <errno.h>
> +#include <getopt.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <stdarg.h>
> +
> +#include "main.h"
> +
> +const char *bin_name;
> +static int last_argc;
> +static char **last_argv;
> +static int (*last_do_help)(int argc, char **argv);
> +json_writer_t *json_wtr;
> +bool pretty_output;
> +bool json_output;
> +
> +static void __attribute__((noreturn)) clean_and_exit(int i)
> +{
> + if (json_output)
> + jsonw_destroy(&json_wtr);
> +
> + exit(i);
> +}
> +
> +void usage(void)
> +{
> + last_do_help(last_argc - 1, last_argv + 1);
> +
> + clean_and_exit(-1);
> +}
> +
> +static int do_help(int argc __attribute__((unused)),
> + char **argv __attribute__((unused)))
> +{
> + if (json_output) {
> + jsonw_null(json_wtr);
> + return 0;
> + }
> +
> + fprintf(stderr,
> + "Usage: %s [OPTIONS] OBJECT { COMMAND | help }\n"
> + " %s version\n"
> + "\n"
> + " OBJECT := { }\n"
> + " " HELP_SPEC_OPTIONS "\n"
> + "",
> + bin_name, bin_name);
> +
> + return 0;
> +}
> +
> +static int do_version(int argc __attribute__((unused)),
> + char **argv __attribute__((unused)))
> +{
> + if (json_output) {
> + jsonw_start_object(json_wtr);
> + jsonw_name(json_wtr, "version");
> + jsonw_printf(json_wtr, "\"0.1.0\"");
Any reason not to start with something like commit 4bfe3bd3cc35
("tools/bpftool: use version from the kernel source tree") here?
Otherwise I doubt 0.1.0 will be changed ever.
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH net-next 2/5] tools: ynltool: create skeleton for the C command
2025-11-06 16:37 ` Stanislav Fomichev
@ 2025-11-06 22:24 ` Jakub Kicinski
0 siblings, 0 replies; 11+ messages in thread
From: Jakub Kicinski @ 2025-11-06 22:24 UTC (permalink / raw)
To: Stanislav Fomichev
Cc: davem, donald.hunter, netdev, edumazet, pabeni, andrew+netdev,
horms, sdf, joe, jstancek
On Thu, 6 Nov 2025 08:37:07 -0800 Stanislav Fomichev wrote:
> > +static int do_version(int argc __attribute__((unused)),
> > + char **argv __attribute__((unused)))
> > +{
> > + if (json_output) {
> > + jsonw_start_object(json_wtr);
> > + jsonw_name(json_wtr, "version");
> > + jsonw_printf(json_wtr, "\"0.1.0\"");
>
> Any reason not to start with something like commit 4bfe3bd3cc35
> ("tools/bpftool: use version from the kernel source tree") here?
> Otherwise I doubt 0.1.0 will be changed ever.
Ah, thanks a lot for the pointer! I grepped bpftool for kernelversion
but somehow it eluded me..
--
pw-bot: cr
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH net-next 3/5] tools: ynltool: add page-pool stats
2025-11-04 23:23 [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
2025-11-04 23:23 ` [PATCH net-next 1/5] netlink: specs: netdev add missing stats to qstat-get Jakub Kicinski
2025-11-04 23:23 ` [PATCH net-next 2/5] tools: ynltool: create skeleton for the C command Jakub Kicinski
@ 2025-11-04 23:23 ` Jakub Kicinski
2025-11-04 23:23 ` [PATCH net-next 4/5] tools: ynltool: add qstats support Jakub Kicinski
` (3 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Jakub Kicinski @ 2025-11-04 23:23 UTC (permalink / raw)
To: davem, donald.hunter
Cc: netdev, edumazet, pabeni, andrew+netdev, horms, sdf, joe,
jstancek, Jakub Kicinski
Replace the page-pool sample with page pool support in ynltool.
# ynltool page-pool stats
eth0[2] page pools: 18 (zombies: 0)
refs: 171456 bytes: 702283776 (refs: 0 bytes: 0)
recycling: 97.3% (alloc: 2679:6134966 recycle: 1250981:4719386)
# ynltool -j page-pool stats | jq
[
{
"ifname": "eth0",
"ifindex": 2,
"page_pools": 18,
"zombies": 0,
"live": {
"refs": 171456,
"bytes": 702283776
},
"zombie": {
"refs": 0,
"bytes": 0
},
"recycling_pct": 97.2746,
"alloc": {
"slow": 2679,
"fast": 6135029
},
"recycle": {
"ring": 1250997,
"cache": 4719432
}
}
]
# ynltool page-pool stats group-by pp
pool id: 108 dev: eth0[2] napi: 530
inflight: 9472 pages 38797312 bytes
recycling: 95.5% (alloc: 148:208379 recycle: 45386:153842)
pool id: 107 dev: eth0[2] napi: 529
inflight: 9408 pages 38535168 bytes
recycling: 94.9% (alloc: 147:180178 recycle: 42251:128808)
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
tools/net/ynl/ynltool/Makefile | 11 +-
tools/net/ynl/ynltool/main.h | 3 +
tools/net/ynl/samples/page-pool.c | 149 ----------
tools/net/ynl/ynltool/main.c | 3 +-
tools/net/ynl/ynltool/page-pool.c | 461 ++++++++++++++++++++++++++++++
5 files changed, 474 insertions(+), 153 deletions(-)
delete mode 100644 tools/net/ynl/samples/page-pool.c
create mode 100644 tools/net/ynl/ynltool/page-pool.c
diff --git a/tools/net/ynl/ynltool/Makefile b/tools/net/ynl/ynltool/Makefile
index ce27dc691ffe..1e860c63df66 100644
--- a/tools/net/ynl/ynltool/Makefile
+++ b/tools/net/ynl/ynltool/Makefile
@@ -10,25 +10,30 @@ CFLAGS := -Wall -Wextra -Werror -O2
ifeq ("$(DEBUG)","1")
CFLAGS += -g -fsanitize=address -fsanitize=leak -static-libasan
endif
-CFLAGS += -I../lib
+CFLAGS += -I../lib -I../generated -I../../../include/uapi/
SRCS := $(wildcard *.c)
OBJS := $(patsubst %.c,$(OUTPUT)%.o,$(SRCS))
YNLTOOL := $(OUTPUT)ynltool
+LIBS := ../lib/ynl.a ../generated/netdev-user.o
include $(wildcard *.d)
all: $(YNLTOOL)
-$(YNLTOOL): $(OBJS)
+$(YNLTOOL): $(OBJS) $(LIBS)
@echo -e "\tLINK $@"
- @$(CC) $(CFLAGS) -o $@ $(OBJS)
+ @$(CC) $(CFLAGS) -o $@ $(OBJS) $(LIBS) -lmnl
%.o: %.c main.h json_writer.h
@echo -e "\tCC $@"
@$(COMPILE.c) -MMD -c -o $@ $<
+../generated/netdev-user.o: ../generated/netdev-user.c
+ @echo -e "\tCC $@"
+ @$(CC) $(filter-out -Werror -fsanitize=address -fsanitize=leak -static-libasan,$(CFLAGS)) -MMD -c -o $@ $<
+
clean:
rm -f *.o *.d *~
diff --git a/tools/net/ynl/ynltool/main.h b/tools/net/ynl/ynltool/main.h
index f4a70acf2085..fd05d21451a2 100644
--- a/tools/net/ynl/ynltool/main.h
+++ b/tools/net/ynl/ynltool/main.h
@@ -59,4 +59,7 @@ struct cmd {
int cmd_select(const struct cmd *cmds, int argc, char **argv,
int (*help)(int argc, char **argv));
+/* subcommands */
+int do_page_pool(int argc, char **argv);
+
#endif /* __YNLTOOL_H */
diff --git a/tools/net/ynl/samples/page-pool.c b/tools/net/ynl/samples/page-pool.c
deleted file mode 100644
index e5d521320fbf..000000000000
--- a/tools/net/ynl/samples/page-pool.c
+++ /dev/null
@@ -1,149 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0
-#define _GNU_SOURCE
-
-#include <stdio.h>
-#include <string.h>
-
-#include <ynl.h>
-
-#include <net/if.h>
-
-#include "netdev-user.h"
-
-struct stat {
- unsigned int ifc;
-
- struct {
- unsigned int cnt;
- size_t refs, bytes;
- } live[2];
-
- size_t alloc_slow, alloc_fast, recycle_ring, recycle_cache;
-};
-
-struct stats_array {
- unsigned int i, max;
- struct stat *s;
-};
-
-static struct stat *find_ifc(struct stats_array *a, unsigned int ifindex)
-{
- unsigned int i;
-
- for (i = 0; i < a->i; i++) {
- if (a->s[i].ifc == ifindex)
- return &a->s[i];
- }
-
- a->i++;
- if (a->i == a->max) {
- a->max *= 2;
- a->s = reallocarray(a->s, a->max, sizeof(*a->s));
- }
- a->s[i].ifc = ifindex;
- return &a->s[i];
-}
-
-static void count(struct stat *s, unsigned int l,
- struct netdev_page_pool_get_rsp *pp)
-{
- s->live[l].cnt++;
- if (pp->_present.inflight)
- s->live[l].refs += pp->inflight;
- if (pp->_present.inflight_mem)
- s->live[l].bytes += pp->inflight_mem;
-}
-
-int main(int argc, char **argv)
-{
- struct netdev_page_pool_stats_get_list *pp_stats;
- struct netdev_page_pool_get_list *pools;
- struct stats_array a = {};
- struct ynl_error yerr;
- struct ynl_sock *ys;
-
- ys = ynl_sock_create(&ynl_netdev_family, &yerr);
- if (!ys) {
- fprintf(stderr, "YNL: %s\n", yerr.msg);
- return 1;
- }
-
- a.max = 128;
- a.s = calloc(a.max, sizeof(*a.s));
- if (!a.s)
- goto err_close;
-
- pools = netdev_page_pool_get_dump(ys);
- if (!pools)
- goto err_free;
-
- ynl_dump_foreach(pools, pp) {
- struct stat *s = find_ifc(&a, pp->ifindex);
-
- count(s, 1, pp);
- if (pp->_present.detach_time)
- count(s, 0, pp);
- }
- netdev_page_pool_get_list_free(pools);
-
- pp_stats = netdev_page_pool_stats_get_dump(ys);
- if (!pp_stats)
- goto err_free;
-
- ynl_dump_foreach(pp_stats, pp) {
- struct stat *s = find_ifc(&a, pp->info.ifindex);
-
- if (pp->_present.alloc_fast)
- s->alloc_fast += pp->alloc_fast;
- if (pp->_present.alloc_refill)
- s->alloc_fast += pp->alloc_refill;
- if (pp->_present.alloc_slow)
- s->alloc_slow += pp->alloc_slow;
- if (pp->_present.recycle_ring)
- s->recycle_ring += pp->recycle_ring;
- if (pp->_present.recycle_cached)
- s->recycle_cache += pp->recycle_cached;
- }
- netdev_page_pool_stats_get_list_free(pp_stats);
-
- for (unsigned int i = 0; i < a.i; i++) {
- char ifname[IF_NAMESIZE];
- struct stat *s = &a.s[i];
- const char *name;
- double recycle;
-
- if (!s->ifc) {
- name = "<orphan>\t";
- } else {
- name = if_indextoname(s->ifc, ifname);
- if (name)
- printf("%8s", name);
- printf("[%u]\t", s->ifc);
- }
-
- printf("page pools: %u (zombies: %u)\n",
- s->live[1].cnt, s->live[0].cnt);
- printf("\t\trefs: %zu bytes: %zu (refs: %zu bytes: %zu)\n",
- s->live[1].refs, s->live[1].bytes,
- s->live[0].refs, s->live[0].bytes);
-
- /* We don't know how many pages are sitting in cache and ring
- * so we will under-count the recycling rate a bit.
- */
- recycle = (double)(s->recycle_ring + s->recycle_cache) /
- (s->alloc_fast + s->alloc_slow) * 100;
- printf("\t\trecycling: %.1lf%% (alloc: %zu:%zu recycle: %zu:%zu)\n",
- recycle, s->alloc_slow, s->alloc_fast,
- s->recycle_ring, s->recycle_cache);
- }
-
- ynl_sock_destroy(ys);
- return 0;
-
-err_free:
- free(a.s);
-err_close:
- fprintf(stderr, "YNL: %s\n", ys->err.msg);
- ynl_sock_destroy(ys);
- return 2;
-}
diff --git a/tools/net/ynl/ynltool/main.c b/tools/net/ynl/ynltool/main.c
index c5047fad50cf..ba7420e2a7d5 100644
--- a/tools/net/ynl/ynltool/main.c
+++ b/tools/net/ynl/ynltool/main.c
@@ -47,7 +47,7 @@ static int do_help(int argc __attribute__((unused)),
"Usage: %s [OPTIONS] OBJECT { COMMAND | help }\n"
" %s version\n"
"\n"
- " OBJECT := { }\n"
+ " OBJECT := { page-pool }\n"
" " HELP_SPEC_OPTIONS "\n"
"",
bin_name, bin_name);
@@ -71,6 +71,7 @@ static int do_version(int argc __attribute__((unused)),
static const struct cmd commands[] = {
{ "help", do_help },
+ { "page-pool", do_page_pool },
{ "version", do_version },
{ 0 }
};
diff --git a/tools/net/ynl/ynltool/page-pool.c b/tools/net/ynl/ynltool/page-pool.c
new file mode 100644
index 000000000000..4b24492abab7
--- /dev/null
+++ b/tools/net/ynl/ynltool/page-pool.c
@@ -0,0 +1,461 @@
+// 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 <ynl.h>
+#include "netdev-user.h"
+
+#include "main.h"
+
+struct pp_stat {
+ unsigned int ifc;
+
+ struct {
+ unsigned int cnt;
+ size_t refs, bytes;
+ } live[2];
+
+ size_t alloc_slow, alloc_fast, recycle_ring, recycle_cache;
+};
+
+struct pp_stats_array {
+ unsigned int i, max;
+ struct pp_stat *s;
+};
+
+static struct pp_stat *find_ifc(struct pp_stats_array *a, unsigned int ifindex)
+{
+ unsigned int i;
+
+ for (i = 0; i < a->i; i++) {
+ if (a->s[i].ifc == ifindex)
+ return &a->s[i];
+ }
+
+ a->i++;
+ if (a->i == a->max) {
+ a->max *= 2;
+ a->s = reallocarray(a->s, a->max, sizeof(*a->s));
+ }
+ a->s[i].ifc = ifindex;
+ return &a->s[i];
+}
+
+static void count_pool(struct pp_stat *s, unsigned int l,
+ struct netdev_page_pool_get_rsp *pp)
+{
+ s->live[l].cnt++;
+ if (pp->_present.inflight)
+ s->live[l].refs += pp->inflight;
+ if (pp->_present.inflight_mem)
+ s->live[l].bytes += pp->inflight_mem;
+}
+
+/* We don't know how many pages are sitting in cache and ring
+ * so we will under-count the recycling rate a bit.
+ */
+static void print_json_recycling_stats(struct pp_stat *s)
+{
+ double recycle;
+
+ if (s->alloc_fast + s->alloc_slow) {
+ recycle = (double)(s->recycle_ring + s->recycle_cache) /
+ (s->alloc_fast + s->alloc_slow) * 100;
+ jsonw_float_field(json_wtr, "recycling_pct", recycle);
+ }
+
+ jsonw_name(json_wtr, "alloc");
+ jsonw_start_object(json_wtr);
+ jsonw_uint_field(json_wtr, "slow", s->alloc_slow);
+ jsonw_uint_field(json_wtr, "fast", s->alloc_fast);
+ jsonw_end_object(json_wtr);
+
+ jsonw_name(json_wtr, "recycle");
+ jsonw_start_object(json_wtr);
+ jsonw_uint_field(json_wtr, "ring", s->recycle_ring);
+ jsonw_uint_field(json_wtr, "cache", s->recycle_cache);
+ jsonw_end_object(json_wtr);
+}
+
+static void print_plain_recycling_stats(struct pp_stat *s)
+{
+ double recycle;
+
+ if (s->alloc_fast + s->alloc_slow) {
+ recycle = (double)(s->recycle_ring + s->recycle_cache) /
+ (s->alloc_fast + s->alloc_slow) * 100;
+ printf("recycling: %.1lf%% (alloc: %zu:%zu recycle: %zu:%zu)",
+ recycle, s->alloc_slow, s->alloc_fast,
+ s->recycle_ring, s->recycle_cache);
+ }
+}
+
+static void print_json_stats(struct pp_stats_array *a)
+{
+ jsonw_start_array(json_wtr);
+
+ for (unsigned int i = 0; i < a->i; i++) {
+ char ifname[IF_NAMESIZE];
+ struct pp_stat *s = &a->s[i];
+ const char *name;
+
+ jsonw_start_object(json_wtr);
+
+ if (!s->ifc) {
+ jsonw_string_field(json_wtr, "ifname", "<orphan>");
+ jsonw_uint_field(json_wtr, "ifindex", 0);
+ } else {
+ name = if_indextoname(s->ifc, ifname);
+ if (name)
+ jsonw_string_field(json_wtr, "ifname", name);
+ jsonw_uint_field(json_wtr, "ifindex", s->ifc);
+ }
+
+ jsonw_uint_field(json_wtr, "page_pools", s->live[1].cnt);
+ jsonw_uint_field(json_wtr, "zombies", s->live[0].cnt);
+
+ jsonw_name(json_wtr, "live");
+ jsonw_start_object(json_wtr);
+ jsonw_uint_field(json_wtr, "refs", s->live[1].refs);
+ jsonw_uint_field(json_wtr, "bytes", s->live[1].bytes);
+ jsonw_end_object(json_wtr);
+
+ jsonw_name(json_wtr, "zombie");
+ jsonw_start_object(json_wtr);
+ jsonw_uint_field(json_wtr, "refs", s->live[0].refs);
+ jsonw_uint_field(json_wtr, "bytes", s->live[0].bytes);
+ jsonw_end_object(json_wtr);
+
+ if (s->alloc_fast || s->alloc_slow)
+ print_json_recycling_stats(s);
+
+ jsonw_end_object(json_wtr);
+ }
+
+ jsonw_end_array(json_wtr);
+}
+
+static void print_plain_stats(struct pp_stats_array *a)
+{
+ for (unsigned int i = 0; i < a->i; i++) {
+ char ifname[IF_NAMESIZE];
+ struct pp_stat *s = &a->s[i];
+ const char *name;
+
+ if (!s->ifc) {
+ printf("<orphan>\t");
+ } else {
+ name = if_indextoname(s->ifc, ifname);
+ if (name)
+ printf("%8s", name);
+ printf("[%u]\t", s->ifc);
+ }
+
+ printf("page pools: %u (zombies: %u)\n",
+ s->live[1].cnt, s->live[0].cnt);
+ printf("\t\trefs: %zu bytes: %zu (refs: %zu bytes: %zu)\n",
+ s->live[1].refs, s->live[1].bytes,
+ s->live[0].refs, s->live[0].bytes);
+
+ if (s->alloc_fast || s->alloc_slow) {
+ printf("\t\t");
+ print_plain_recycling_stats(s);
+ printf("\n");
+ }
+ }
+}
+
+static bool
+find_pool_stat_in_list(struct netdev_page_pool_stats_get_list *pp_stats,
+ __u64 pool_id, struct pp_stat *pstat)
+{
+ ynl_dump_foreach(pp_stats, pp) {
+ if (!pp->_present.info || !pp->info._present.id)
+ continue;
+ if (pp->info.id != pool_id)
+ continue;
+
+ memset(pstat, 0, sizeof(*pstat));
+ if (pp->_present.alloc_fast)
+ pstat->alloc_fast = pp->alloc_fast;
+ if (pp->_present.alloc_refill)
+ pstat->alloc_fast += pp->alloc_refill;
+ if (pp->_present.alloc_slow)
+ pstat->alloc_slow = pp->alloc_slow;
+ if (pp->_present.recycle_ring)
+ pstat->recycle_ring = pp->recycle_ring;
+ if (pp->_present.recycle_cached)
+ pstat->recycle_cache = pp->recycle_cached;
+ return true;
+ }
+ return false;
+}
+
+static void
+print_json_pool_list(struct netdev_page_pool_get_list *pools,
+ struct netdev_page_pool_stats_get_list *pp_stats,
+ bool zombies_only)
+{
+ jsonw_start_array(json_wtr);
+
+ ynl_dump_foreach(pools, pp) {
+ char ifname[IF_NAMESIZE];
+ struct pp_stat pstat;
+ const char *name;
+
+ if (zombies_only && !pp->_present.detach_time)
+ continue;
+
+ jsonw_start_object(json_wtr);
+
+ jsonw_uint_field(json_wtr, "id", pp->id);
+
+ if (pp->_present.ifindex) {
+ name = if_indextoname(pp->ifindex, ifname);
+ if (name)
+ jsonw_string_field(json_wtr, "ifname", name);
+ jsonw_uint_field(json_wtr, "ifindex", pp->ifindex);
+ }
+
+ if (pp->_present.napi_id)
+ jsonw_uint_field(json_wtr, "napi_id", pp->napi_id);
+
+ if (pp->_present.inflight)
+ jsonw_uint_field(json_wtr, "refs", pp->inflight);
+
+ if (pp->_present.inflight_mem)
+ jsonw_uint_field(json_wtr, "bytes", pp->inflight_mem);
+
+ if (pp->_present.detach_time)
+ jsonw_uint_field(json_wtr, "detach_time", pp->detach_time);
+
+ if (pp->_present.dmabuf)
+ jsonw_uint_field(json_wtr, "dmabuf", pp->dmabuf);
+
+ if (find_pool_stat_in_list(pp_stats, pp->id, &pstat) &&
+ (pstat.alloc_fast || pstat.alloc_slow))
+ print_json_recycling_stats(&pstat);
+
+ jsonw_end_object(json_wtr);
+ }
+
+ jsonw_end_array(json_wtr);
+}
+
+static void
+print_plain_pool_list(struct netdev_page_pool_get_list *pools,
+ struct netdev_page_pool_stats_get_list *pp_stats,
+ bool zombies_only)
+{
+ ynl_dump_foreach(pools, pp) {
+ char ifname[IF_NAMESIZE];
+ struct pp_stat pstat;
+ const char *name;
+
+ if (zombies_only && !pp->_present.detach_time)
+ continue;
+
+ printf("pool id: %llu", pp->id);
+
+ if (pp->_present.ifindex) {
+ name = if_indextoname(pp->ifindex, ifname);
+ if (name)
+ printf(" dev: %s", name);
+ printf("[%u]", pp->ifindex);
+ }
+
+ if (pp->_present.napi_id)
+ printf(" napi: %llu", pp->napi_id);
+
+ printf("\n");
+
+ if (pp->_present.inflight || pp->_present.inflight_mem) {
+ printf(" inflight:");
+ if (pp->_present.inflight)
+ printf(" %llu pages", pp->inflight);
+ if (pp->_present.inflight_mem)
+ printf(" %llu bytes", pp->inflight_mem);
+ printf("\n");
+ }
+
+ if (pp->_present.detach_time)
+ printf(" detached: %llu\n", pp->detach_time);
+
+ if (pp->_present.dmabuf)
+ printf(" dmabuf: %u\n", pp->dmabuf);
+
+ if (find_pool_stat_in_list(pp_stats, pp->id, &pstat) &&
+ (pstat.alloc_fast || pstat.alloc_slow)) {
+ printf(" ");
+ print_plain_recycling_stats(&pstat);
+ printf("\n");
+ }
+ }
+}
+
+static void aggregate_device_stats(struct pp_stats_array *a,
+ struct netdev_page_pool_get_list *pools,
+ struct netdev_page_pool_stats_get_list *pp_stats)
+{
+ ynl_dump_foreach(pools, pp) {
+ struct pp_stat *s = find_ifc(a, pp->ifindex);
+
+ count_pool(s, 1, pp);
+ if (pp->_present.detach_time)
+ count_pool(s, 0, pp);
+ }
+
+ ynl_dump_foreach(pp_stats, pp) {
+ struct pp_stat *s = find_ifc(a, pp->info.ifindex);
+
+ if (pp->_present.alloc_fast)
+ s->alloc_fast += pp->alloc_fast;
+ if (pp->_present.alloc_refill)
+ s->alloc_fast += pp->alloc_refill;
+ if (pp->_present.alloc_slow)
+ s->alloc_slow += pp->alloc_slow;
+ if (pp->_present.recycle_ring)
+ s->recycle_ring += pp->recycle_ring;
+ if (pp->_present.recycle_cached)
+ s->recycle_cache += pp->recycle_cached;
+ }
+}
+
+static int do_stats(int argc, char **argv)
+{
+ struct netdev_page_pool_stats_get_list *pp_stats;
+ struct netdev_page_pool_get_list *pools;
+ enum {
+ GROUP_BY_DEVICE,
+ GROUP_BY_POOL,
+ } group_by = GROUP_BY_DEVICE;
+ bool zombies_only = false;
+ struct pp_stats_array a = {};
+ struct ynl_error yerr;
+ struct ynl_sock *ys;
+ int ret = 0;
+
+ /* Parse options */
+ while (argc > 0) {
+ if (is_prefix(*argv, "group-by")) {
+ NEXT_ARG();
+
+ if (!REQ_ARGS(1))
+ return -1;
+
+ if (is_prefix(*argv, "device")) {
+ group_by = GROUP_BY_DEVICE;
+ } else if (is_prefix(*argv, "pp") ||
+ is_prefix(*argv, "page-pool") ||
+ is_prefix(*argv, "none")) {
+ group_by = GROUP_BY_POOL;
+ } else {
+ p_err("invalid group-by value '%s'", *argv);
+ return -1;
+ }
+ NEXT_ARG();
+ } else if (is_prefix(*argv, "zombies")) {
+ zombies_only = true;
+ group_by = GROUP_BY_POOL;
+ NEXT_ARG();
+ } else {
+ p_err("unknown option '%s'", *argv);
+ return -1;
+ }
+ }
+
+ ys = ynl_sock_create(&ynl_netdev_family, &yerr);
+ if (!ys) {
+ p_err("YNL: %s", yerr.msg);
+ return -1;
+ }
+
+ pools = netdev_page_pool_get_dump(ys);
+ if (!pools) {
+ p_err("failed to get page pools: %s", ys->err.msg);
+ ret = -1;
+ goto exit_close;
+ }
+
+ pp_stats = netdev_page_pool_stats_get_dump(ys);
+ if (!pp_stats) {
+ p_err("failed to get page pool stats: %s", ys->err.msg);
+ ret = -1;
+ goto exit_free_pp_list;
+ }
+
+ /* If grouping by pool, print individual pools */
+ if (group_by == GROUP_BY_POOL) {
+ if (json_output)
+ print_json_pool_list(pools, pp_stats, zombies_only);
+ else
+ print_plain_pool_list(pools, pp_stats, zombies_only);
+ } else {
+ /* Aggregated stats mode (group-by device) */
+ a.max = 64;
+ a.s = calloc(a.max, sizeof(*a.s));
+ if (!a.s) {
+ p_err("failed to allocate stats array");
+ ret = -1;
+ goto exit_free_stats_list;
+ }
+
+ aggregate_device_stats(&a, pools, pp_stats);
+
+ if (json_output)
+ print_json_stats(&a);
+ else
+ print_plain_stats(&a);
+
+ free(a.s);
+ }
+
+exit_free_stats_list:
+ netdev_page_pool_stats_get_list_free(pp_stats);
+exit_free_pp_list:
+ netdev_page_pool_get_list_free(pools);
+exit_close:
+ ynl_sock_destroy(ys);
+ return ret;
+}
+
+static int do_help(int argc __attribute__((unused)),
+ char **argv __attribute__((unused)))
+{
+ if (json_output) {
+ jsonw_null(json_wtr);
+ return 0;
+ }
+
+ fprintf(stderr,
+ "Usage: %s page-pool { COMMAND | help }\n"
+ " %s page-pool stats [ OPTIONS ]\n"
+ "\n"
+ " OPTIONS := { group-by { device | page-pool | none } | zombies }\n"
+ "\n"
+ " stats - Display page pool statistics\n"
+ " stats group-by device - Group statistics by network device (default)\n"
+ " stats group-by page-pool | pp | none\n"
+ " - Show individual page pool details (no grouping)\n"
+ " stats zombies - Show only zombie page pools (detached but with\n"
+ " pages in flight). Implies group-by page-pool.\n"
+ "",
+ bin_name, bin_name);
+
+ return 0;
+}
+
+static const struct cmd page_pool_cmds[] = {
+ { "help", do_help },
+ { "stats", do_stats },
+ { 0 }
+};
+
+int do_page_pool(int argc, char **argv)
+{
+ return cmd_select(page_pool_cmds, argc, argv, do_help);
+}
--
2.51.1
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net-next 4/5] tools: ynltool: add qstats support
2025-11-04 23:23 [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
` (2 preceding siblings ...)
2025-11-04 23:23 ` [PATCH net-next 3/5] tools: ynltool: add page-pool stats Jakub Kicinski
@ 2025-11-04 23:23 ` Jakub Kicinski
2025-11-04 23:23 ` [PATCH net-next 5/5] tools: ynltool: add traffic distribution balance Jakub Kicinski
` (2 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Jakub Kicinski @ 2025-11-04 23:23 UTC (permalink / raw)
To: davem, donald.hunter
Cc: netdev, edumazet, pabeni, andrew+netdev, horms, sdf, joe,
jstancek, Jakub Kicinski
$ ynltool qstat
eth0 rx-packets: 493192163 rx-bytes: 1442544543997
tx-packets: 745999838 tx-bytes: 4574215826482
tx-stop: 7033 tx-wake: 7033
$ ynltool qstat show group-by queue
eth0 rx-0 packets: 70196880 bytes: 178633973750
eth0 rx-1 packets: 63623419 bytes: 197274745250
...
eth0 tx-1 packets: 98645810 bytes: 631247647938
stop: 1048 wake: 1048
eth0 tx-2 packets: 86775824 bytes: 563930471952
stop: 1126 wake: 1126
...
$ ynltool -j qstat | jq
[
{
"ifname": "eth0",
"ifindex": 2,
"rx": {
"packets": 493396439,
"bytes": 1443608198921
},
"tx": {
"packets": 746239978,
"bytes": 4574333772645,
"stop": 7072,
"wake": 7072
}
}
]
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
tools/net/ynl/ynltool/main.h | 1 +
tools/net/ynl/ynltool/main.c | 3 +-
tools/net/ynl/ynltool/qstats.c | 330 +++++++++++++++++++++++++++++++++
3 files changed, 333 insertions(+), 1 deletion(-)
create mode 100644 tools/net/ynl/ynltool/qstats.c
diff --git a/tools/net/ynl/ynltool/main.h b/tools/net/ynl/ynltool/main.h
index fd05d21451a2..c7039f9ac55a 100644
--- a/tools/net/ynl/ynltool/main.h
+++ b/tools/net/ynl/ynltool/main.h
@@ -61,5 +61,6 @@ int cmd_select(const struct cmd *cmds, int argc, char **argv,
/* subcommands */
int do_page_pool(int argc, char **argv);
+int do_qstats(int argc, char **argv);
#endif /* __YNLTOOL_H */
diff --git a/tools/net/ynl/ynltool/main.c b/tools/net/ynl/ynltool/main.c
index ba7420e2a7d5..1a69f0ae1647 100644
--- a/tools/net/ynl/ynltool/main.c
+++ b/tools/net/ynl/ynltool/main.c
@@ -47,7 +47,7 @@ static int do_help(int argc __attribute__((unused)),
"Usage: %s [OPTIONS] OBJECT { COMMAND | help }\n"
" %s version\n"
"\n"
- " OBJECT := { page-pool }\n"
+ " OBJECT := { page-pool | qstats }\n"
" " HELP_SPEC_OPTIONS "\n"
"",
bin_name, bin_name);
@@ -72,6 +72,7 @@ static int do_version(int argc __attribute__((unused)),
static const struct cmd commands[] = {
{ "help", do_help },
{ "page-pool", do_page_pool },
+ { "qstats", do_qstats },
{ "version", do_version },
{ 0 }
};
diff --git a/tools/net/ynl/ynltool/qstats.c b/tools/net/ynl/ynltool/qstats.c
new file mode 100644
index 000000000000..fcdbb6d9a852
--- /dev/null
+++ b/tools/net/ynl/ynltool/qstats.c
@@ -0,0 +1,330 @@
+// 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 <ynl.h>
+#include "netdev-user.h"
+
+#include "main.h"
+
+static enum netdev_qstats_scope scope; /* default - device */
+
+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 int do_show(int argc, char **argv)
+{
+ struct netdev_qstats_get_list *qstats;
+ struct netdev_qstats_get_req *req;
+ struct ynl_error yerr;
+ struct ynl_sock *ys;
+ int ret = 0;
+
+ /* 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;
+ }
+ }
+
+ ys = ynl_sock_create(&ynl_netdev_family, &yerr);
+ if (!ys) {
+ p_err("YNL: %s", yerr.msg);
+ return -1;
+ }
+
+ req = netdev_qstats_get_req_alloc();
+ if (!req) {
+ p_err("failed to allocate qstats request");
+ ret = -1;
+ goto exit_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);
+ ret = -1;
+ goto exit_close;
+ }
+
+ /* 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);
+exit_close:
+ ynl_sock_destroy(ys);
+ return ret;
+}
+
+static int do_help(int argc __attribute__((unused)),
+ char **argv __attribute__((unused)))
+{
+ if (json_output) {
+ jsonw_null(json_wtr);
+ return 0;
+ }
+
+ fprintf(stderr,
+ "Usage: %s qstats { COMMAND | help }\n"
+ " %s qstats [ show ] [ OPTIONS ]\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"
+ "",
+ bin_name, bin_name);
+
+ return 0;
+}
+
+static const struct cmd qstats_cmds[] = {
+ { "show", do_show },
+ { "help", do_help },
+ { 0 }
+};
+
+int do_qstats(int argc, char **argv)
+{
+ return cmd_select(qstats_cmds, argc, argv, do_help);
+}
--
2.51.1
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net-next 5/5] tools: ynltool: add traffic distribution balance
2025-11-04 23:23 [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
` (3 preceding siblings ...)
2025-11-04 23:23 ` [PATCH net-next 4/5] tools: ynltool: add qstats support Jakub Kicinski
@ 2025-11-04 23:23 ` Jakub Kicinski
2025-11-06 15:02 ` [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
2025-11-07 16:10 ` patchwork-bot+netdevbpf
6 siblings, 0 replies; 11+ messages in thread
From: Jakub Kicinski @ 2025-11-04 23:23 UTC (permalink / raw)
To: davem, donald.hunter
Cc: netdev, edumazet, pabeni, andrew+netdev, horms, sdf, joe,
jstancek, Jakub Kicinski
The main if not only use case for per-queue stats today is checking
for traffic imbalance. Add simple traffic balance analysis to qstats.
$ ynltool qstat balance
eth0 rx 44 queues:
rx-packets : cv=6.9% ns=24.2% stddev=512006493
min=6278921110 max=8011570575 mean=7437054644
rx-bytes : cv=6.9% ns=24.1% stddev=759670503060
min=9326315769440 max=11884393670786 mean=11035439201354
...
$ ynltool -j qstat balance | jq
[
{
"ifname": "eth0",
"ifindex": 2,
"queue-type": "rx",
"rx-packets": {
"queue-count": 44,
"min": 6278301665,
"max": 8010780185,
"mean": 7.43635E+9,
"stddev": 5.12012E+8,
"coefficient-of-variation": 6.88525,
"normalized-spread": 24.249
},
...
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
tools/net/ynl/ynltool/Makefile | 2 +-
tools/net/ynl/ynltool/qstats.c | 293 ++++++++++++++++++++++++++++++++-
2 files changed, 293 insertions(+), 2 deletions(-)
diff --git a/tools/net/ynl/ynltool/Makefile b/tools/net/ynl/ynltool/Makefile
index 1e860c63df66..2fe520f54ebb 100644
--- a/tools/net/ynl/ynltool/Makefile
+++ b/tools/net/ynl/ynltool/Makefile
@@ -24,7 +24,7 @@ all: $(YNLTOOL)
$(YNLTOOL): $(OBJS) $(LIBS)
@echo -e "\tLINK $@"
- @$(CC) $(CFLAGS) -o $@ $(OBJS) $(LIBS) -lmnl
+ @$(CC) $(CFLAGS) -o $@ $(OBJS) $(LIBS) -lmnl -lm
%.o: %.c main.h json_writer.h
@echo -e "\tCC $@"
diff --git a/tools/net/ynl/ynltool/qstats.c b/tools/net/ynl/ynltool/qstats.c
index fcdbb6d9a852..31fb45709ffa 100644
--- a/tools/net/ynl/ynltool/qstats.c
+++ b/tools/net/ynl/ynltool/qstats.c
@@ -5,6 +5,7 @@
#include <string.h>
#include <errno.h>
#include <net/if.h>
+#include <math.h>
#include <ynl.h>
#include "netdev-user.h"
@@ -13,6 +14,16 @@
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);
@@ -293,6 +304,283 @@ static int do_show(int argc, char **argv)
return ret;
}
+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_req *req;
+ struct netdev_qstats_get_rsp **sorted;
+ struct ynl_error yerr;
+ struct ynl_sock *ys;
+ unsigned int count = 0;
+ unsigned int i, j;
+ int ret = 0;
+
+ if (argc > 0) {
+ p_err("balance command takes no arguments");
+ return -1;
+ }
+
+ ys = ynl_sock_create(&ynl_netdev_family, &yerr);
+ if (!ys) {
+ p_err("YNL: %s", yerr.msg);
+ return -1;
+ }
+
+ req = netdev_qstats_get_req_alloc();
+ if (!req) {
+ p_err("failed to allocate qstats request");
+ ret = -1;
+ goto exit_close;
+ }
+
+ /* Always use queue scope for balance analysis */
+ netdev_qstats_get_req_set_scope(req, NETDEV_QSTATS_SCOPE_QUEUE);
+
+ 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);
+ ret = -1;
+ goto exit_close;
+ }
+
+ /* 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);
+exit_close:
+ ynl_sock_destroy(ys);
+ return ret;
+}
+
static int do_help(int argc __attribute__((unused)),
char **argv __attribute__((unused)))
{
@@ -304,6 +592,7 @@ static int do_help(int argc __attribute__((unused)),
fprintf(stderr,
"Usage: %s qstats { COMMAND | help }\n"
" %s qstats [ show ] [ OPTIONS ]\n"
+ " %s qstats balance\n"
"\n"
" OPTIONS := { scope queue | group-by { device | queue } }\n"
"\n"
@@ -312,14 +601,16 @@ static int do_help(int argc __attribute__((unused)),
" 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"
+ " balance - Analyze traffic distribution balance.\n"
"",
- bin_name, bin_name);
+ bin_name, bin_name, bin_name);
return 0;
}
static const struct cmd qstats_cmds[] = {
{ "show", do_show },
+ { "balance", do_balance },
{ "help", do_help },
{ 0 }
};
--
2.51.1
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool
2025-11-04 23:23 [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
` (4 preceding siblings ...)
2025-11-04 23:23 ` [PATCH net-next 5/5] tools: ynltool: add traffic distribution balance Jakub Kicinski
@ 2025-11-06 15:02 ` Jakub Kicinski
2025-11-06 16:34 ` Stanislav Fomichev
2025-11-07 16:10 ` patchwork-bot+netdevbpf
6 siblings, 1 reply; 11+ messages in thread
From: Jakub Kicinski @ 2025-11-06 15:02 UTC (permalink / raw)
To: davem, donald.hunter
Cc: netdev, edumazet, pabeni, andrew+netdev, horms, sdf, joe,
jstancek
On Tue, 4 Nov 2025 15:23:43 -0800 Jakub Kicinski wrote:
> The page-pool YNL sample is quite useful. It's helps calculate
> recycling rate and memory consumption. Since we still haven't
> figured out a way to integrate with iproute2 (not for the lack
> of thinking how to solve it) - create a ynltool command in ynl.
>
> Add page-pool and qstats support.
>
> Most commands can use the Python YNL CLI directly but low level
> stats often need aggregation or some math on top to be useful.
> Specifically in this patch set:
> - page pool stats are aggregated and recycling rate computed
> - per-queue stats are used to compute traffic balance across queues
FWIW I'd appreciate any feedback here. It's hard to draw the line
on what to implement in a tool like ynltool and what to leave for
the YNL Python directly.
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool
2025-11-06 15:02 ` [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
@ 2025-11-06 16:34 ` Stanislav Fomichev
0 siblings, 0 replies; 11+ messages in thread
From: Stanislav Fomichev @ 2025-11-06 16:34 UTC (permalink / raw)
To: Jakub Kicinski
Cc: davem, donald.hunter, netdev, edumazet, pabeni, andrew+netdev,
horms, sdf, joe, jstancek
On 11/06, Jakub Kicinski wrote:
> On Tue, 4 Nov 2025 15:23:43 -0800 Jakub Kicinski wrote:
> > The page-pool YNL sample is quite useful. It's helps calculate
> > recycling rate and memory consumption. Since we still haven't
> > figured out a way to integrate with iproute2 (not for the lack
> > of thinking how to solve it) - create a ynltool command in ynl.
> >
> > Add page-pool and qstats support.
> >
> > Most commands can use the Python YNL CLI directly but low level
> > stats often need aggregation or some math on top to be useful.
> > Specifically in this patch set:
> > - page pool stats are aggregated and recycling rate computed
> > - per-queue stats are used to compute traffic balance across queues
>
> FWIW I'd appreciate any feedback here. It's hard to draw the line
> on what to implement in a tool like ynltool and what to leave for
> the YNL Python directly.
I like the idea. Python is useful for local development, but shipping
it (as of now) is a bit painful. I'm not sure what's the line gonna
be between iproute2 vs ethtool vs ynltool, but I think we can figure
it out as we go.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool
2025-11-04 23:23 [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
` (5 preceding siblings ...)
2025-11-06 15:02 ` [PATCH net-next 0/5] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
@ 2025-11-07 16:10 ` patchwork-bot+netdevbpf
6 siblings, 0 replies; 11+ messages in thread
From: patchwork-bot+netdevbpf @ 2025-11-07 16:10 UTC (permalink / raw)
To: Jakub Kicinski
Cc: davem, donald.hunter, netdev, edumazet, pabeni, andrew+netdev,
horms, sdf, joe, jstancek
Hello:
This series was applied to netdev/net-next.git (main)
by Jakub Kicinski <kuba@kernel.org>:
On Tue, 4 Nov 2025 15:23:43 -0800 you wrote:
> The page-pool YNL sample is quite useful. It's helps calculate
> recycling rate and memory consumption. Since we still haven't
> figured out a way to integrate with iproute2 (not for the lack
> of thinking how to solve it) - create a ynltool command in ynl.
>
> Add page-pool and qstats support.
>
> [...]
Here is the summary with links:
- [net-next,1/5] netlink: specs: netdev add missing stats to qstat-get
https://git.kernel.org/netdev/net-next/c/c6934c4e049c
- [net-next,2/5] tools: ynltool: create skeleton for the C command
(no matching commit)
- [net-next,3/5] tools: ynltool: add page-pool stats
(no matching commit)
- [net-next,4/5] tools: ynltool: add qstats support
(no matching commit)
- [net-next,5/5] tools: ynltool: add traffic distribution balance
(no matching commit)
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 11+ messages in thread