netdev.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool
@ 2025-11-07 16:22 Jakub Kicinski
  2025-11-07 16:22 ` [PATCH net-next v2 1/4] tools: ynltool: create skeleton for the C command Jakub Kicinski
                   ` (5 more replies)
  0 siblings, 6 replies; 9+ messages in thread
From: Jakub Kicinski @ 2025-11-07 16:22 UTC (permalink / raw)
  To: davem, donald.hunter
  Cc: netdev, edumazet, pabeni, andrew+netdev, horms, sdf, joe,
	jstancek, Jakub Kicinski

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

v2:
 - patch 1 was applied already
 - [patch 1 (was 2)] use kernel version
 - [patch 2 (was 3)] Makefile cleanup
v1: https://lore.kernel.org/20251104232348.1954349-1-kuba@kernel.org

Jakub Kicinski (4):
  tools: ynltool: create skeleton for the C command
  tools: ynltool: add page-pool stats
  tools: ynltool: add qstats support
  tools: ynltool: add traffic distribution balance

 tools/net/ynl/Makefile              |   3 +-
 tools/net/ynl/ynltool/Makefile      |  55 +++
 tools/net/ynl/ynltool/json_writer.h |  75 ++++
 tools/net/ynl/ynltool/main.h        |  66 +++
 tools/net/ynl/samples/page-pool.c   | 149 -------
 tools/net/ynl/ynltool/json_writer.c | 288 +++++++++++++
 tools/net/ynl/ynltool/main.c        | 242 +++++++++++
 tools/net/ynl/ynltool/page-pool.c   | 461 +++++++++++++++++++++
 tools/net/ynl/ynltool/qstats.c      | 621 ++++++++++++++++++++++++++++
 tools/net/ynl/ynltool/.gitignore    |   1 +
 10 files changed, 1811 insertions(+), 150 deletions(-)
 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
 delete mode 100644 tools/net/ynl/samples/page-pool.c
 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/page-pool.c
 create mode 100644 tools/net/ynl/ynltool/qstats.c
 create mode 100644 tools/net/ynl/ynltool/.gitignore

-- 
2.51.1


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH net-next v2 1/4] tools: ynltool: create skeleton for the C command
  2025-11-07 16:22 [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
@ 2025-11-07 16:22 ` Jakub Kicinski
  2025-11-11 11:15   ` Paolo Abeni
  2025-11-07 16:22 ` [PATCH net-next v2 2/4] tools: ynltool: add page-pool stats Jakub Kicinski
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 9+ messages in thread
From: Jakub Kicinski @ 2025-11-07 16:22 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>
---
v2:
 - use kernel source version
v1: https://lore.kernel.org/20251104232348.1954349-3-kuba@kernel.org
---
 tools/net/ynl/Makefile              |   3 +-
 tools/net/ynl/ynltool/Makefile      |  52 +++++
 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, 720 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..cfabab3a20da
--- /dev/null
+++ b/tools/net/ynl/ynltool/Makefile
@@ -0,0 +1,52 @@
+# 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
+
+SRC_VERSION := \
+	$(shell make --no-print-directory -sC ../../../.. kernelversion || \
+		echo "unknown")
+
+CFLAGS += -DSRC_VERSION='"$(SRC_VERSION)"'
+
+SRCS := $(wildcard *.c)
+OBJS := $(patsubst %.c,$(OUTPUT)%.o,$(SRCS))
+
+YNLTOOL := $(OUTPUT)ynltool
+
+include $(wildcard *.d)
+
+all: $(YNLTOOL)
+
+Q = @
+
+$(YNLTOOL): $(OBJS)
+	$(Q)echo -e "\tLINK $@"
+	$(Q)$(CC) $(CFLAGS) -o $@ $(OBJS)
+
+%.o: %.c main.h json_writer.h
+	$(Q)echo -e "\tCC $@"
+	$(Q)$(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..8e15e4ee543f
--- /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, SRC_VERSION);
+		jsonw_end_object(json_wtr);
+	} else {
+		printf("%s " SRC_VERSION "\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] 9+ messages in thread

* [PATCH net-next v2 2/4] tools: ynltool: add page-pool stats
  2025-11-07 16:22 [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
  2025-11-07 16:22 ` [PATCH net-next v2 1/4] tools: ynltool: create skeleton for the C command Jakub Kicinski
@ 2025-11-07 16:22 ` Jakub Kicinski
  2025-11-07 16:22 ` [PATCH net-next v2 3/4] tools: ynltool: add qstats support Jakub Kicinski
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: Jakub Kicinski @ 2025-11-07 16:22 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>
---
v2:
 - cleanup the Makefile deps a little
v1: https://lore.kernel.org/20251104232348.1954349-4-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, 473 insertions(+), 154 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 cfabab3a20da..11240740ed81 100644
--- a/tools/net/ynl/ynltool/Makefile
+++ b/tools/net/ynl/ynltool/Makefile
@@ -10,7 +10,7 @@ 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/
 
 SRC_VERSION := \
 	$(shell make --no-print-directory -sC ../../../.. kernelversion || \
@@ -29,14 +29,17 @@ all: $(YNLTOOL)
 
 Q = @
 
-$(YNLTOOL): $(OBJS)
+$(YNLTOOL): ../libynl.a $(OBJS)
 	$(Q)echo -e "\tLINK $@"
-	$(Q)$(CC) $(CFLAGS) -o $@ $(OBJS)
+	$(Q)$(CC) $(CFLAGS) -o $@ $(OBJS) ../libynl.a -lmnl
 
-%.o: %.c main.h json_writer.h
+%.o: %.c ../libynl.a
 	$(Q)echo -e "\tCC $@"
 	$(Q)$(COMPILE.c) -MMD -c -o $@ $<
 
+../libynl.a:
+	$(Q)$(MAKE) -C ../
+
 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 8e15e4ee543f..f83c6f3245c8 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] 9+ messages in thread

* [PATCH net-next v2 3/4] tools: ynltool: add qstats support
  2025-11-07 16:22 [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
  2025-11-07 16:22 ` [PATCH net-next v2 1/4] tools: ynltool: create skeleton for the C command Jakub Kicinski
  2025-11-07 16:22 ` [PATCH net-next v2 2/4] tools: ynltool: add page-pool stats Jakub Kicinski
@ 2025-11-07 16:22 ` Jakub Kicinski
  2025-11-07 16:22 ` [PATCH net-next v2 4/4] tools: ynltool: add traffic distribution balance Jakub Kicinski
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: Jakub Kicinski @ 2025-11-07 16:22 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 f83c6f3245c8..5d0f428eed0a 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] 9+ messages in thread

* [PATCH net-next v2 4/4] tools: ynltool: add traffic distribution balance
  2025-11-07 16:22 [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
                   ` (2 preceding siblings ...)
  2025-11-07 16:22 ` [PATCH net-next v2 3/4] tools: ynltool: add qstats support Jakub Kicinski
@ 2025-11-07 16:22 ` Jakub Kicinski
  2025-11-07 21:46 ` [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Stanislav Fomichev
  2025-11-11 11:50 ` patchwork-bot+netdevbpf
  5 siblings, 0 replies; 9+ messages in thread
From: Jakub Kicinski @ 2025-11-07 16:22 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 11240740ed81..86c30b7420cf 100644
--- a/tools/net/ynl/ynltool/Makefile
+++ b/tools/net/ynl/ynltool/Makefile
@@ -31,7 +31,7 @@ Q = @
 
 $(YNLTOOL): ../libynl.a $(OBJS)
 	$(Q)echo -e "\tLINK $@"
-	$(Q)$(CC) $(CFLAGS) -o $@ $(OBJS) ../libynl.a -lmnl
+	$(Q)$(CC) $(CFLAGS) -o $@ $(OBJS) ../libynl.a -lmnl -lm
 
 %.o: %.c ../libynl.a
 	$(Q)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] 9+ messages in thread

* Re: [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool
  2025-11-07 16:22 [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
                   ` (3 preceding siblings ...)
  2025-11-07 16:22 ` [PATCH net-next v2 4/4] tools: ynltool: add traffic distribution balance Jakub Kicinski
@ 2025-11-07 21:46 ` Stanislav Fomichev
  2025-11-11 11:50 ` patchwork-bot+netdevbpf
  5 siblings, 0 replies; 9+ messages in thread
From: Stanislav Fomichev @ 2025-11-07 21:46 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: davem, donald.hunter, netdev, edumazet, pabeni, andrew+netdev,
	horms, sdf, joe, jstancek

On 11/07, 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
> 
> v2:
>  - patch 1 was applied already
>  - [patch 1 (was 2)] use kernel version
>  - [patch 2 (was 3)] Makefile cleanup

Acked-by: Stanislav Fomichev <sdf@fomichev.me>

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH net-next v2 1/4] tools: ynltool: create skeleton for the C command
  2025-11-07 16:22 ` [PATCH net-next v2 1/4] tools: ynltool: create skeleton for the C command Jakub Kicinski
@ 2025-11-11 11:15   ` Paolo Abeni
  2025-11-11 11:46     ` Paolo Abeni
  0 siblings, 1 reply; 9+ messages in thread
From: Paolo Abeni @ 2025-11-11 11:15 UTC (permalink / raw)
  To: Jakub Kicinski, davem, donald.hunter
  Cc: netdev, edumazet, andrew+netdev, horms, sdf, joe, jstancek

On 11/7/25 5:22 PM, 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

FTR, it took me a little to understand that for this patch at least is
"inspired" alike the notorious MS socket implementation ;)

> 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>
> ---
> v2:
>  - use kernel source version
> v1: https://lore.kernel.org/20251104232348.1954349-3-kuba@kernel.org
> ---
>  tools/net/ynl/Makefile              |   3 +-
>  tools/net/ynl/ynltool/Makefile      |  52 +++++
>  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, 720 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..cfabab3a20da
> --- /dev/null
> +++ b/tools/net/ynl/ynltool/Makefile
> @@ -0,0 +1,52 @@
> +# 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
> +
> +SRC_VERSION := \
> +	$(shell make --no-print-directory -sC ../../../.. kernelversion || \
> +		echo "unknown")
> +
> +CFLAGS += -DSRC_VERSION='"$(SRC_VERSION)"'
> +
> +SRCS := $(wildcard *.c)
> +OBJS := $(patsubst %.c,$(OUTPUT)%.o,$(SRCS))
> +
> +YNLTOOL := $(OUTPUT)ynltool
> +
> +include $(wildcard *.d)
> +
> +all: $(YNLTOOL)
> +
> +Q = @
> +
> +$(YNLTOOL): $(OBJS)
> +	$(Q)echo -e "\tLINK $@"
> +	$(Q)$(CC) $(CFLAGS) -o $@ $(OBJS)
> +
> +%.o: %.c main.h json_writer.h
> +	$(Q)echo -e "\tCC $@"
> +	$(Q)$(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)

Minor nit: $(INSTALL) above?

Also possibly using/including scripts/Makefile.include could avoid some
code duplication? (or at least make the V=1 option effective)/

/P


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH net-next v2 1/4] tools: ynltool: create skeleton for the C command
  2025-11-11 11:15   ` Paolo Abeni
@ 2025-11-11 11:46     ` Paolo Abeni
  0 siblings, 0 replies; 9+ messages in thread
From: Paolo Abeni @ 2025-11-11 11:46 UTC (permalink / raw)
  To: Jakub Kicinski, davem, donald.hunter
  Cc: netdev, edumazet, andrew+netdev, horms, sdf, joe, jstancek

On 11/11/25 12:15 PM, Paolo Abeni wrote:
> On 11/7/25 5:22 PM, Jakub Kicinski wrote:
>> +install: $(YNLTOOL)
>> +	install -m 0755 $(YNLTOOL) $(DESTDIR)$(bindir)/$(YNLTOOL)
> 
> Minor nit: $(INSTALL) above?
> 
> Also possibly using/including scripts/Makefile.include could avoid some
> code duplication? (or at least make the V=1 option effective)/

All very minor. I think it's better to follow-up (if agreed) than to repost.

/P


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool
  2025-11-07 16:22 [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
                   ` (4 preceding siblings ...)
  2025-11-07 21:46 ` [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Stanislav Fomichev
@ 2025-11-11 11:50 ` patchwork-bot+netdevbpf
  5 siblings, 0 replies; 9+ messages in thread
From: patchwork-bot+netdevbpf @ 2025-11-11 11:50 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 Paolo Abeni <pabeni@redhat.com>:

On Fri,  7 Nov 2025 08:22:23 -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,v2,1/4] tools: ynltool: create skeleton for the C command
    https://git.kernel.org/netdev/net-next/c/b02d229013aa
  - [net-next,v2,2/4] tools: ynltool: add page-pool stats
    https://git.kernel.org/netdev/net-next/c/124dac9b421c
  - [net-next,v2,3/4] tools: ynltool: add qstats support
    https://git.kernel.org/netdev/net-next/c/3f0a638d45fc
  - [net-next,v2,4/4] tools: ynltool: add traffic distribution balance
    https://git.kernel.org/netdev/net-next/c/9eef97a9dea3

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] 9+ messages in thread

end of thread, other threads:[~2025-11-11 11:50 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-11-07 16:22 [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Jakub Kicinski
2025-11-07 16:22 ` [PATCH net-next v2 1/4] tools: ynltool: create skeleton for the C command Jakub Kicinski
2025-11-11 11:15   ` Paolo Abeni
2025-11-11 11:46     ` Paolo Abeni
2025-11-07 16:22 ` [PATCH net-next v2 2/4] tools: ynltool: add page-pool stats Jakub Kicinski
2025-11-07 16:22 ` [PATCH net-next v2 3/4] tools: ynltool: add qstats support Jakub Kicinski
2025-11-07 16:22 ` [PATCH net-next v2 4/4] tools: ynltool: add traffic distribution balance Jakub Kicinski
2025-11-07 21:46 ` [PATCH net-next v2 0/4] tools: ynl: turn the page-pool sample into a real tool Stanislav Fomichev
2025-11-11 11:50 ` patchwork-bot+netdevbpf

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).