From: Stanislav Fomichev <sdf.kernel@gmail.com>
To: netdev@vger.kernel.org
Cc: davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com
Subject: [RFC net-next v2 1/5] selftests: net: py: add timestamped metric output
Date: Mon, 5 Oct 2026 13:11:10 -0700 [thread overview]
Message-ID: <20261005201115.2690830-2-sdf@fomichev.me> (raw)
In-Reply-To: <20261005201115.2690830-1-sdf@fomichev.me>
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/
---
.../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
next prev parent reply other threads:[~2026-10-05 20:11 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-05 20:11 [RFC net-next v2 0/5] selftests: net: add performance metric reporting Stanislav Fomichev
2026-10-05 20:11 ` Stanislav Fomichev [this message]
2026-10-05 20:11 ` [RFC net-next v2 2/5] selftests: net: py: add metric policy output Stanislav Fomichev
2026-10-05 20:11 ` [RFC net-next v2 3/5] selftests: drv-net: add an iperf performance test Stanislav Fomichev
2026-10-05 20:11 ` [RFC net-next v2 4/5] selftests: drv-net: add a kperf runner Stanislav Fomichev
2026-10-05 20:11 ` [RFC net-next v2 5/5] selftests: drv-net: measure devmem performance with kperf Stanislav Fomichev
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261005201115.2690830-2-sdf@fomichev.me \
--to=sdf.kernel@gmail.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=kuba@kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox