Thread (10 messages) 10 messages, 3 authors, 15d ago

[RFC net-next 3/6] selftests: drv-net: add a system performance monitor

flat view

From: Stanislav Fomichev <hidden>
Date: 2026-09-16 19:04: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 a context manager which emits per-CPU time, utilization, and NIC drop
counters for both endpoints. Use one persistent remote process for
one-second sampling and enable benchmark metrics while it is active.

Tested on two mlx5 hosts. First local CPU sample:

  # # ktap-metric-policy-json: {"aggregation":"busy-core-equivalents","display_label":"Percent of one CPU","display_range":{"max":100.0,"min":0.0},"display_scale":100.0,"name":"cpu.time.usr","summarize":"p90","transform":"rate"}
  # # ktap-metric-policy-json: {"aggregation":"busy-core-equivalents","display_label":"Percent of one CPU","display_range":{"max":100.0,"min":0.0},"display_scale":100.0,"name":"cpu.time.sys","summarize":"p90","transform":"rate"}
  # # ktap-metric-policy-json: {"aggregation":"busy-core-equivalents","display_label":"Percent of one CPU","display_range":{"max":100.0,"min":0.0},"display_scale":100.0,"name":"cpu.time.sirq","summarize":"p90","transform":"rate"}
  # # ktap-metric-policy-json: {"aggregation":"busy-core-equivalents","display_label":"Percent of one CPU","display_range":{"max":100.0,"min":0.0},"name":"cpu.utilization","summarize":"p90"}

  # # ktap-metric-json: {"host":"local","name":"cpu.time.usr","shape":"per-cpu","time":1.615669,"value":[0.0,0.0,0.02,0.0,0.0,0.01,0.0,0.0,0.01,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.06,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.02,0.0,0.16,0.0,0.01,0.01,0.0,0.0,0.0,0.01,0.01,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.01,0.0,0.0,0.02,0.01,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.01,0.0,0.0,0.01,0.0,0.03,0.0,0.0,0.0,0.0,0.01,0.02,0.06,0.01,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.02,0.01,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.02,0.0,0.0,0.0,0.0,0.02,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.04,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.02,0.0,0.0,0.0,0.0,0.01]}
  # # ktap-metric-json: {"host":"local","name":"cpu.time.sys","shape":"per-cpu","time":1.615747,"value":[0.0,0.0,0.0,0.0,0.0,0.02,0.0,0.0,0.0,0.01,0.01,0.0,0.0,0.01,0.0,0.01,0.01,0.0,0.0,0.04,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.01,0.02,0.0,0.0,0.0,0.01,0.13,0.0,0.02,0.01,0.0,0.02,0.01,0.01,0.02,0.0,0.01,0.02,0.0,0.04,0.0,0.0,0.01,0.01,0.01,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.01,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.01,0.06,0.02,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.01,0.01,0.0,0.0,0.0,0.0,0.0,0.01,0.02,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.01,0.01,0.02,0.0,0.0,0.0,0.02,0.0,0.0,0.0,0.0,0.0,0.02,0.01,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.15,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.01]}
  # # ktap-metric-json: {"host":"local","name":"cpu.time.sirq","shape":"per-cpu","time":1.615814,"value":[0.09,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.08,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.01,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]}
  # # ktap-metric-json: {"host":"local","name":"cpu.utilization","shape":"per-cpu","time":1.615881,"value":[9.0909,0.0,2.0202,0.0,0.0,3.0,0.0,0.9901,0.9901,1.0,2.9412,0.0,0.0,0.9901,0.0,1.0,0.9901,0.0,0.0,3.9604,0.0,0.0,0.0,0.0,0.0,0.0,0.9901,0.0,0.0,0.0,0.0,1.0,0.0,6.0,0.0,8.9109,0.0,1.0,2.0,0.9901,0.0,2.0,2.9412,30.0,0.0,3.9604,2.9412,0.0,2.0202,1.0,2.0,2.9703,0.0,1.0101,2.0,0.0,5.0,0.0,0.0,1.0,2.9126,0.9901,0.9901,2.0,0.9901,0.0,0.0,0.9901,0.0,0.0,0.0,0.0,0.0,0.0,0.9901,2.0,0.9901,0.0,0.0,0.9901,0.0,3.9604,0.0,0.0,0.0,0.0,1.0,3.0,12.0,3.0,0.0,0.0,1.0,0.0,0.0,1.9802,0.0,0.0,2.0,0.0,0.9901,0.0,0.9901,0.0,0.0,0.9901,0.0,1.9802,0.9901,0.0,0.0,0.9901,1.0,0.9901,0.9901,0.0,0.0,0.0,0.0,0.9901,0.9901,3.8835,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9901,0.9901,0.0,0.9901,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.9608,0.0,0.0,0.0,1.9802,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.9802,0.0,0.0,1.0,1.0,2.9412,2.0,0.0,1.0,0.0,2.0202,0.0,0.0,0.0,0.0,0.0,2.9703,0.9901,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9901,0.0,0.0,0.0,0.0,1.9802,0.0,0.0,0.0,0.9901,0.0,0.0,19.1919,0.0,0.0,0.0,0.0,1.0,0.9901,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9901,0.9901,1.9802,0.0,0.0,0.0,0.0,1.9802]}
  # ok 1 iperf.test_iperf
  # # Totals: pass:1 fail:0 xfail:0 xpass:0 skip:0 error:0

Signed-off-by: Stanislav Fomichev <sdf@fomichev.me>
---
 .../drivers/net/hw/lib/py/__init__.py         |  11 +-
 .../selftests/drivers/net/lib/py/__init__.py  |   9 +-
 .../drivers/net/lib/py/system_monitor.py      | 240 ++++++++++++++++++
 .../testing/selftests/net/lib/py/__init__.py  |   5 +-
 tools/testing/selftests/net/lib/py/ksft.py    |  14 +-
 5 files changed, 269 insertions(+), 10 deletions(-)
 create mode 100644 tools/testing/selftests/drivers/net/lib/py/system_monitor.py
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 349647c2307d..1077930d1677 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -28,11 +28,13 @@ KSFT_DIR = (Path(__file__).parent / "../../../../..").resolve()
     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_metric, \
-        ksft_metric_aggregate, ksft_pr, ksft_run, ksft_setup, \
+        ksft_metric_aggregate, ksft_metric_reporting, 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
+    from drivers.net.lib.py import GenerateTraffic, Remote, SystemMonitor, \
+        Iperf3Runner
     from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
 
     __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
@@ -45,14 +47,15 @@ KSFT_DIR = (Path(__file__).parent / "../../../../..").resolve()
                "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
                "KsftSkipEx", "KsftFailEx", "KsftXfailEx",
                "ksft_disruptive", "ksft_exit", "ksft_metric",
-               "ksft_metric_aggregate", "ksft_pr", "ksft_run",
+               "ksft_metric_aggregate", "ksft_metric_reporting", "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",
                "NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
-               "Remote", "Iperf3Runner"]
+               "Remote", "Iperf3Runner", "SystemMonitor"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
     print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index afad9d5392ca..d779f48ea6c5 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
@@ -28,7 +28,8 @@ KSFT_DIR = (Path(__file__).parent / "../../../..").resolve()
     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_metric, \
-        ksft_metric_aggregate, ksft_pr, ksft_run, ksft_setup, \
+        ksft_metric_aggregate, ksft_metric_reporting, 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
@@ -43,7 +44,8 @@ KSFT_DIR = (Path(__file__).parent / "../../../..").resolve()
                "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
                "KsftSkipEx", "KsftFailEx", "KsftXfailEx",
                "ksft_disruptive", "ksft_exit", "ksft_metric",
-               "ksft_metric_aggregate", "ksft_pr", "ksft_run",
+               "ksft_metric_aggregate", "ksft_metric_reporting", "ksft_pr",
+               "ksft_run",
                "ksft_setup", "ksft_variants",
                "KsftNamedVariant",
                "ksft_eq", "ksft_ge", "ksft_in", "ksft_is", "ksft_lt",
@@ -52,10 +54,11 @@ KSFT_DIR = (Path(__file__).parent / "../../../..").resolve()
 
     from .env import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
     from .load import GenerateTraffic, Iperf3Runner
+    from .system_monitor import SystemMonitor
     from .remote import Remote
 
     __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
-                "Remote", "Iperf3Runner"]
+                "Remote", "Iperf3Runner", "SystemMonitor"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
     print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/system_monitor.py b/tools/testing/selftests/drivers/net/lib/py/system_monitor.py
new file mode 100644
index 000000000000..81ded4bea1b9
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/lib/py/system_monitor.py
@@ -0,0 +1,240 @@
+# SPDX-License-Identifier: GPL-2.0
+
+"""Performance measurement helpers for network driver selftests."""
+
+import shlex
+import subprocess
+import threading
+import time
+
+from lib.py import cmd, ksft_metric, ksft_metric_aggregate, \
+    ksft_metric_reporting, ksft_pr
+
+
+class SystemMonitor:
+    """Emit CPU and NIC metrics for the local and remote test endpoints."""
+
+    _CPU_FIELDS = ("user", "nice", "sys", "idle", "iowait", "irq", "sirq",
+                   "steal")
+    _CPU_TIME_METRICS = {
+        "cpu.time.usr": ("user", "nice"),
+        "cpu.time.sys": ("sys",),
+        "cpu.time.sirq": ("sirq",),
+    }
+    _NIC_METRICS = {
+        "rx_dropped": "nic.rx.dropped",
+        "rx_missed_errors": "nic.rx.missed_errors",
+        "tx_dropped": "nic.tx.dropped",
+    }
+    _SNAPSHOT_END = "__KSFT_SYSTEM_STAT_END__"
+    _NIC_STATS = "__KSFT_NIC_STATS__"
+    _CLOCK_TICKS = 100
+    _SAMPLE_INTERVAL = 1
+
+    def __init__(self, env):
+        self.env = env
+        self._remote_sampler = None
+
+    @classmethod
+    def _parse_cpu(cls, text):
+        stats = {}
+        for line in text.splitlines():
+            fields = line.split()
+            if not fields or not fields[0].startswith("cpu") or \
+                    not fields[0][3:].isdigit():
+                continue
+            stats[int(fields[0][3:])] = {
+                name: int(value)
+                for name, value in zip(cls._CPU_FIELDS, fields[1:])
+            }
+        return stats
+
+    @classmethod
+    def _parse_nic(cls, text):
+        fields = next(line.split() for line in text.splitlines()
+                      if line.startswith(cls._NIC_STATS))
+        return {name: int(value)
+                for name, value in zip(cls._NIC_METRICS, fields[1:])}
+
+    @classmethod
+    def _read_nic(cls, ifname):
+        stats = {}
+        for name in cls._NIC_METRICS:
+            path = f"/sys/class/net/{ifname}/statistics/{name}"
+            with open(path, encoding="utf-8") as stat_file:
+                stats[name] = int(stat_file.read())
+        return stats
+
+    @classmethod
+    def _cpu_deltas(cls, before, after):
+        return {
+            cpu: {
+                field: after[cpu][field] - before[cpu][field]
+                for field in cls._CPU_FIELDS
+            }
+            for cpu in before.keys() & after.keys()
+        }
+
+    @staticmethod
+    def _per_cpu_array(values):
+        if not values:
+            return None
+        result = [None] * (max(values) + 1)
+        for cpu, value in values.items():
+            result[cpu] = value
+        return result
+
+    def _start_remote_sampler(self):
+        nic_reads = ""
+        for name in self._NIC_METRICS:
+            path = shlex.quote(
+                f"/sys/class/net/{self.env.remote_ifname}/statistics/{name}")
+            nic_reads += (f"read value < {path} || exit; "
+                          f"printf ' %s' \"$value\"; ")
+        command = (
+            "while :; do cat /proc/stat || exit; "
+            f"printf '{self._NIC_STATS}'; {nic_reads}"
+            f"printf '\\n{self._SNAPSHOT_END}\\n'; sleep 1 || exit; done"
+        )
+        self._remote_sampler = cmd(command, host=self.env.remote,
+                                   background=True)
+
+    def _read_remote(self):
+        lines = []
+        while True:
+            line = self._remote_sampler.proc.stdout.readline()
+            if not line:
+                status = self._remote_sampler.proc.poll()
+                raise RuntimeError(
+                    f"Remote system sampler stopped unexpectedly ({status})")
+            line = line.decode("utf-8", "replace").rstrip("\n")
+            if line == self._SNAPSHOT_END:
+                return "\n".join(lines)
+            lines.append(line)
+
+    def _stop_remote_sampler(self):
+        sampler = self._remote_sampler
+        self._remote_sampler = None
+        if sampler is None:
+            return
+        try:
+            sampler.process(terminate=True, fail=False, timeout=5)
+        except subprocess.TimeoutExpired:
+            sampler.proc.kill()
+            sampler.process(terminate=False, fail=False, timeout=5)
+
+    def _snapshot(self):
+        remote = self._read_remote()
+        with open("/proc/stat", encoding="utf-8") as stat_file:
+            local_cpu = self._parse_cpu(stat_file.read())
+        return {
+            "local": {
+                "cpu": local_cpu,
+                "nic": self._read_nic(self.env.ifname),
+            },
+            "remote": {
+                "cpu": self._parse_cpu(remote),
+                "nic": self._parse_nic(remote),
+            },
+        }
+
+    def _record_sample(self, snapshots):
+        for host, current in snapshots.items():
+            baseline = self._baseline[host]
+            previous = self._previous[host]
+            elapsed = self._cpu_deltas(baseline["cpu"], current["cpu"])
+
+            for metric, fields in self._CPU_TIME_METRICS.items():
+                values = self._per_cpu_array({
+                    cpu: sum(delta[field] for field in fields) /
+                    self._CLOCK_TICKS
+                    for cpu, delta in elapsed.items()
+                })
+                if values is not None:
+                    ksft_metric(metric, values, shape="per-cpu", host=host)
+
+            interval = self._cpu_deltas(previous["cpu"], current["cpu"])
+            utilization = {}
+            for cpu, delta in interval.items():
+                total = sum(delta.values())
+                if total > 0:
+                    busy = total - delta["idle"] - delta["iowait"]
+                    utilization[cpu] = round(100 * busy / total, 4)
+            values = self._per_cpu_array(utilization)
+            if values is not None:
+                ksft_metric("cpu.utilization", values, shape="per-cpu",
+                            host=host)
+
+            for counter, metric in self._NIC_METRICS.items():
+                value = current["nic"][counter] - baseline["nic"][counter]
+                ksft_metric(metric, value, shape="scalar", host=host)
+
+        self._previous = snapshots
+
+    def _register_aggregations(self):
+        for metric in self._CPU_TIME_METRICS:
+            ksft_metric_aggregate(
+                metric, summarize="p90", transform="rate",
+                aggregation="busy-core-equivalents",
+                display_range={"min": 0, "max": 100},
+                display_scale=100,
+                display_label="Percent of one CPU")
+        ksft_metric_aggregate(
+            "cpu.utilization", summarize="p90",
+            aggregation="busy-core-equivalents",
+            display_range={"min": 0, "max": 100},
+            display_label="Percent of one CPU")
+        for metric in self._NIC_METRICS.values():
+            ksft_metric_aggregate(
+                metric, summarize="distribution", transform="rate",
+                display_range={"min": 0})
+
+    def _sample_loop(self):
+        next_sample = time.monotonic() + self._SAMPLE_INTERVAL
+        try:
+            while not self._stop_event.wait(
+                    max(0, next_sample - time.monotonic())):
+                self._record_sample(self._snapshot())
+                next_sample += self._SAMPLE_INTERVAL
+                while next_sample <= time.monotonic():
+                    next_sample += self._SAMPLE_INTERVAL
+        except Exception as error:
+            self._sample_error = error
+
+    def __enter__(self):
+        ksft_metric_reporting(True)
+        try:
+            self._register_aggregations()
+            self._start_remote_sampler()
+            self._baseline = self._snapshot()
+            self._previous = self._baseline
+            for host in self._baseline:
+                for metric in self._NIC_METRICS.values():
+                    ksft_metric(metric, 0, shape="scalar", host=host)
+
+            self._stop_event = threading.Event()
+            self._sample_error = None
+            self._thread = threading.Thread(target=self._sample_loop,
+                                            daemon=True)
+            self._thread.start()
+            return self
+        except Exception:
+            ksft_metric_reporting(False)
+            self._stop_remote_sampler()
+            raise
+
+    def __exit__(self, exc_type, _exc_value, _exc_tb):
+        ksft_metric_reporting(False)
+        try:
+            self._stop_event.set()
+            self._thread.join()
+            if self._sample_error is not None:
+                raise self._sample_error
+            self._record_sample(self._snapshot())
+        except Exception as error:
+            if exc_type is None:
+                raise
+            ksft_pr(f"System metric collection failed: {error}")
+        finally:
+            self._stop_remote_sampler()
+        return False
diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
index 21d89ae76b49..9795a83d662a 100644
--- a/tools/testing/selftests/net/lib/py/__init__.py
+++ b/tools/testing/selftests/net/lib/py/__init__.py
@@ -9,7 +9,7 @@ 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_metric, ksft_metric_aggregate, \
-    ksft_setup, ksft_run, \
+    ksft_metric_reporting, ksft_setup, ksft_run, \
     ksft_exit, ksft_variants, KsftNamedVariant
 from .netns import NetNS, NetNSEnter, UserNetNS
 from .nsim import NetdevSim, NetdevSimDev
@@ -26,7 +26,8 @@ __all__ = ["KSRC",
            "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_metric",
-           "ksft_metric_aggregate", "ksft_setup", "ksft_run",
+           "ksft_metric_aggregate", "ksft_metric_reporting", "ksft_setup",
+           "ksft_run",
            "ksft_exit", "ksft_variants",
            "KsftNamedVariant",
            "NetNS", "NetNSEnter", "UserNetNS",
diff --git a/tools/testing/selftests/net/lib/py/ksft.py b/tools/testing/selftests/net/lib/py/ksft.py
index f0cbc7307117..e44029213e4f 100644
--- a/tools/testing/selftests/net/lib/py/ksft.py
+++ b/tools/testing/selftests/net/lib/py/ksft.py
@@ -21,6 +21,7 @@ KSFT_DISRUPTIVE = True
 KSFT_METRICS = None
 KSFT_METRICS_START = None
 KSFT_METRIC_AGGREGATES = None
+KSFT_METRIC_REPORTING = 0
 KSFT_METRICS_LOCK = threading.Lock()
 
 
@@ -99,6 +100,14 @@ KSFT_METRICS_LOCK = threading.Lock()
     print(pfx, prefixed, **kwargs)
 
 
+def ksft_metric_reporting(enable):
+    """Enter or leave a scope which enables benchmark metric producers."""
+    global KSFT_METRIC_REPORTING
+
+    with KSFT_METRICS_LOCK:
+        KSFT_METRIC_REPORTING += 1 if enable else -1
+
+
 def ksft_metric_aggregate(name, summarize="total", *, aggregation=None,
                           transform=None, regression=None,
                           display_range=None, display_scale=None,
@@ -219,6 +228,7 @@ def ksft_metric_aggregate(name, summarize="total", *, aggregation=None,
 
 def _ksft_flush_metrics():
     global KSFT_METRICS, KSFT_METRICS_START, KSFT_METRIC_AGGREGATES
+    global KSFT_METRIC_REPORTING
 
     with KSFT_METRICS_LOCK:
         metrics = KSFT_METRICS
@@ -226,6 +236,7 @@ def ksft_metric_aggregate(name, summarize="total", *, aggregation=None,
         KSFT_METRICS = None
         KSFT_METRICS_START = None
         KSFT_METRIC_AGGREGATES = None
+        KSFT_METRIC_REPORTING = 0
 
     metric_names = {metric["name"] for metric in metrics or []}
     for name, metadata in (aggregates or {}).items():
@@ -549,7 +560,7 @@ KsftCaseFunction = namedtuple("KsftCaseFunction",
     totals = {"pass": 0, "fail": 0, "skip": 0, "xfail": 0}
 
     global KSFT_RESULT, KSFT_METRICS, KSFT_METRICS_START
-    global KSFT_METRIC_AGGREGATES
+    global KSFT_METRIC_AGGREGATES, KSFT_METRIC_REPORTING
     if KSFT_RESULT is not None:
         raise RuntimeError("ksft_run() can't be called multiple times.")
 
@@ -564,6 +575,7 @@ KsftCaseFunction = namedtuple("KsftCaseFunction",
             KSFT_METRICS = []
             KSFT_METRICS_START = time.monotonic()
             KSFT_METRIC_AGGREGATES = {}
+            KSFT_METRIC_REPORTING = 0
         cnt += 1
         comment = ""
         cnt_key = ""
-- 
2.53.0-Meta
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help