[RFC net-next v2 1/5] selftests: net: py: add timestamped metric output
flat view
From: Stanislav Fomichev <hidden>
Date: 2026-10-05 20:11:17
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> --- v1: https://lore.kernel.org/netdev/20260916190409.1222272-1-sdf@fomichev.me/ (local) --- .../testing/selftests/drivers/net/README.rst | 28 +++++++++++ .../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 | 49 ++++++++++++++++++- 5 files changed, 89 insertions(+), 13 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/README.rst b/tools/testing/selftests/drivers/net/README.rst
index 3fe49bce4f3a..3e3cc028d77a 100644
--- a/tools/testing/selftests/drivers/net/README.rst
+++ b/tools/testing/selftests/drivers/net/README.rst@@ -259,6 +259,34 @@ 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, kind="scalar") + +emits output similar to:: + + # ktap-metric-json: {"name":"throughput","time":1.234,"type":"scalar","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 ``kind`` explicitly; it is emitted as the +``type`` field. Supported kinds are ``scalar`` for one numeric value and +``per-cpu`` for an array indexed by CPU number, with ``None`` for a missing or +offline CPU:: + + ksft_metric("cpu.utilization", [75.0, 22.0, None, 100.0], + kind="per-cpu", host="local") + +The helper does not infer the kind 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 4077f5f1a1be..77e4ef34f8b9 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 set_ethtool_feat
@@ -44,8 +44,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 40ccdcf0c378..f744280dcc37 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..1259bbb6d6c5 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,44 @@ KSFT_DISRUPTIVE = True print(pfx, prefixed, **kwargs) +def ksft_metric(name, value, *, kind, **labels): + """Record a timestamped metric with an explicitly described kind. + + The kind is emitted as the ``type`` field. Supported kinds are ``scalar`` + for one numeric value and ``per-cpu`` for an array indexed by CPU number, + with ``None`` for a missing or offline CPU. + """ + 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, + "type": kind, + "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 +443,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 +454,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 +496,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