[RFC net-next 1/6] selftests: net: py: add timestamped metric output
From: Stanislav Fomichev <hidden>
Date: 2026-09-16 19:04:14
Subsystem:
kernel selftest framework, networking drivers, networking [general], the rest · Maintainers:
Shuah Khan, Shuah Khan, Andrew Lunn, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds
Add ksft_metric() to record timestamped JSON observations for the current test case. Signed-off-by: Stanislav Fomichev <sdf@fomichev.me> --- .../testing/selftests/drivers/net/README.rst | 27 ++++++++++++ .../drivers/net/hw/lib/py/__init__.py | 8 ++-- .../selftests/drivers/net/lib/py/__init__.py | 8 ++-- .../testing/selftests/net/lib/py/__init__.py | 9 ++-- tools/testing/selftests/net/lib/py/ksft.py | 44 ++++++++++++++++++- 5 files changed, 83 insertions(+), 13 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/README.rst b/tools/testing/selftests/drivers/net/README.rst
index 3fe49bce4f3a..a6a8605844eb 100644
--- a/tools/testing/selftests/drivers/net/README.rst
+++ b/tools/testing/selftests/drivers/net/README.rst@@ -259,6 +259,33 @@ ksft_pr() Use ``ksft_pr()`` instead of ``print()`` to avoid breaking TAP format. +ksft_metric() +~~~~~~~~~~~~~ + +Use ``ksft_metric()`` to report a numeric measurement for the current test +case. The helper records the time relative to the start of the case and emits +each observation immediately before the test result as compact JSON. For +example:: + + ksft_metric("throughput", 100.5, shape="scalar", direction="rx") + +emits output similar to:: + + # ktap-metric-json: {"direction":"rx","name":"throughput","shape":"scalar","time":1.234,"value":100.5} + +When ``run_kselftest.sh`` nests the test output, its diagnostic prefix makes +this ``# # ktap-metric-json: <json>``. A background sampler can call +``ksft_metric()`` repeatedly. Pass ``shape`` explicitly to describe the +layout of each value. For per-CPU data, the array index is the CPU number and +``None`` represents a missing or offline CPU:: + + ksft_metric("cpu.utilization", [75.0, 22.0, None, 100.0], + shape="per-cpu", host="local") + +The helper does not infer the shape from the Python value, since an array may +represent something other than per-CPU data. Metrics are measurements only and +do not affect the pass or fail result. + ksft_disruptive ~~~~~~~~~~~~~~~
diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index 81e1d1865cd5..17e3daae10e2 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py@@ -27,8 +27,8 @@ KSFT_DIR = (Path(__file__).parent / "../../../../..").resolve() wait_file, ctl_file_write, tool from net.lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids from net.lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx - from net.lib.py import ksft_disruptive, ksft_exit, ksft_pr, ksft_run, \ - ksft_setup, ksft_variants, KsftNamedVariant + from net.lib.py import ksft_disruptive, ksft_exit, ksft_metric, ksft_pr, \ + ksft_run, ksft_setup, ksft_variants, KsftNamedVariant from net.lib.py import ksft_eq, ksft_ge, ksft_in, ksft_is, ksft_lt, \ ksft_ne, ksft_not_in, ksft_raises, ksft_true, ksft_gt, ksft_not_none from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
@@ -43,8 +43,8 @@ KSFT_DIR = (Path(__file__).parent / "../../../../..").resolve() "wait_port_listen", "wait_file", "ctl_file_write", "tool", "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids", "KsftSkipEx", "KsftFailEx", "KsftXfailEx", - "ksft_disruptive", "ksft_exit", "ksft_pr", "ksft_run", - "ksft_setup", "ksft_variants", "KsftNamedVariant", + "ksft_disruptive", "ksft_exit", "ksft_metric", "ksft_pr", + "ksft_run", "ksft_setup", "ksft_variants", "KsftNamedVariant", "ksft_eq", "ksft_ge", "ksft_in", "ksft_is", "ksft_lt", "ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt", "ksft_not_none", "ksft_not_none",
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index 591b1e6c7eea..d22f36b189e4 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py@@ -27,8 +27,8 @@ KSFT_DIR = (Path(__file__).parent / "../../../..").resolve() wait_file, ctl_file_write from net.lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids from net.lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx - from net.lib.py import ksft_disruptive, ksft_exit, ksft_pr, ksft_run, \ - ksft_setup, ksft_variants, KsftNamedVariant + from net.lib.py import ksft_disruptive, ksft_exit, ksft_metric, ksft_pr, \ + ksft_run, ksft_setup, ksft_variants, KsftNamedVariant from net.lib.py import ksft_eq, ksft_ge, ksft_in, ksft_is, ksft_lt, \ ksft_ne, ksft_not_in, ksft_raises, ksft_true, ksft_gt, ksft_not_none
@@ -41,8 +41,8 @@ KSFT_DIR = (Path(__file__).parent / "../../../..").resolve() "wait_port_listen", "wait_file", "ctl_file_write", "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids", "KsftSkipEx", "KsftFailEx", "KsftXfailEx", - "ksft_disruptive", "ksft_exit", "ksft_pr", "ksft_run", - "ksft_setup", "ksft_variants", "KsftNamedVariant", + "ksft_disruptive", "ksft_exit", "ksft_metric", "ksft_pr", + "ksft_run", "ksft_setup", "ksft_variants", "KsftNamedVariant", "ksft_eq", "ksft_ge", "ksft_in", "ksft_is", "ksft_lt", "ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt", "ksft_not_none", "ksft_not_none"]
diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
index 71df5880b356..3d9e6f6b402c 100644
--- a/tools/testing/selftests/net/lib/py/__init__.py
+++ b/tools/testing/selftests/net/lib/py/__init__.py@@ -8,8 +8,8 @@ from .consts import KSRC from .ksft import KsftFailEx, KsftSkipEx, KsftXfailEx, ksft_pr, ksft_eq, \ ksft_ne, ksft_true, ksft_not_none, ksft_in, ksft_not_in, ksft_is, \ ksft_ge, ksft_gt, ksft_lt, ksft_raises, ksft_busy_wait, \ - ktap_result, ksft_disruptive, ksft_setup, ksft_run, ksft_exit, \ - ksft_variants, KsftNamedVariant + ktap_result, ksft_disruptive, ksft_metric, ksft_setup, ksft_run, \ + ksft_exit, ksft_variants, KsftNamedVariant from .netns import NetNS, NetNSEnter, UserNetNS from .nsim import NetdevSim, NetdevSimDev from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \
@@ -24,8 +24,9 @@ __all__ = ["KSRC", "KsftFailEx", "KsftSkipEx", "KsftXfailEx", "ksft_pr", "ksft_eq", "ksft_ne", "ksft_true", "ksft_not_none", "ksft_in", "ksft_not_in", "ksft_is", "ksft_ge", "ksft_gt", "ksft_lt", "ksft_raises", - "ksft_busy_wait", "ktap_result", "ksft_disruptive", "ksft_setup", - "ksft_run", "ksft_exit", "ksft_variants", "KsftNamedVariant", + "ksft_busy_wait", "ktap_result", "ksft_disruptive", "ksft_metric", + "ksft_setup", "ksft_run", "ksft_exit", "ksft_variants", + "KsftNamedVariant", "NetNS", "NetNSEnter", "UserNetNS", "CmdExitFailure", "fd_read_timeout", "cmd", "bkg", "defer", "bpftool", "ip", "ethtool", "bpftrace", "rand_port", "rand_ports",
diff --git a/tools/testing/selftests/net/lib/py/ksft.py b/tools/testing/selftests/net/lib/py/ksft.py
index 81287c2daff0..fb0865df86c9 100644
--- a/tools/testing/selftests/net/lib/py/ksft.py
+++ b/tools/testing/selftests/net/lib/py/ksft.py@@ -4,9 +4,11 @@ import fnmatch import functools import getopt import inspect +import json import os import signal import sys +import threading import time import traceback from collections import namedtuple
@@ -16,6 +18,9 @@ from . import utils KSFT_RESULT = None KSFT_RESULT_ALL = True KSFT_DISRUPTIVE = True +KSFT_METRICS = None +KSFT_METRICS_START = None +KSFT_METRICS_LOCK = threading.Lock() class KsftFailEx(Exception):
@@ -93,6 +98,39 @@ KSFT_DISRUPTIVE = True print(pfx, prefixed, **kwargs) +def ksft_metric(name, value, *, shape, **labels): + """Record a timestamped metric with an explicitly described shape.""" + with KSFT_METRICS_LOCK: + if KSFT_METRICS is None or KSFT_METRICS_START is None: + raise RuntimeError("ksft_metric() called outside of a test case") + + metric = { + "name": name, + "shape": shape, + "value": value, + "time": round(time.monotonic() - KSFT_METRICS_START, 6), + } + for key, label_value in sorted(labels.items()): + if key in metric: + raise ValueError(f"Metric label uses reserved name: {key}") + metric[key] = label_value + KSFT_METRICS.append(metric) + + +def _ksft_flush_metrics(): + global KSFT_METRICS, KSFT_METRICS_START + + with KSFT_METRICS_LOCK: + metrics = KSFT_METRICS + KSFT_METRICS = None + KSFT_METRICS_START = None + + for metric in metrics or []: + payload = json.dumps(metric, allow_nan=False, separators=(",", ":"), + sort_keys=True) + ksft_pr(f"ktap-metric-json: {payload}") + + def _fail(*args): global KSFT_RESULT KSFT_RESULT = False
@@ -400,7 +438,7 @@ KsftCaseFunction = namedtuple("KsftCaseFunction", totals = {"pass": 0, "fail": 0, "skip": 0, "xfail": 0} - global KSFT_RESULT + global KSFT_RESULT, KSFT_METRICS, KSFT_METRICS_START if KSFT_RESULT is not None: raise RuntimeError("ksft_run() can't be called multiple times.")
@@ -411,6 +449,9 @@ KsftCaseFunction = namedtuple("KsftCaseFunction", stop = False for func, args, name in test_cases: KSFT_RESULT = True + with KSFT_METRICS_LOCK: + KSFT_METRICS = [] + KSFT_METRICS_START = time.monotonic() cnt += 1 comment = "" cnt_key = ""
@@ -450,6 +491,7 @@ KsftCaseFunction = namedtuple("KsftCaseFunction", if not cnt_key: cnt_key = 'pass' if KSFT_RESULT else 'fail' + _ksft_flush_metrics() ktap_result(KSFT_RESULT, cnt, name, comment=comment) totals[cnt_key] += 1
--
2.53.0-Meta