diff --git a/cpu/jitter.py b/cpu/jitter.py new file mode 100644 index 000000000..935c69650 --- /dev/null +++ b/cpu/jitter.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# See LICENSE for more details. +# +# Copyright: 2026 Advanced Micro Devices, Inc. +# Author: Sumit Kumar + +""" +Run the Intel LKP "jitter" PMA instruction-jitter microbenchmark under +Avocado and assert on the maximum instruction jitter (in cycles). +""" + +import json +import os +import socket + +import yaml + +from autils.devel import lkp +from avocado import Test +from avocado.utils.software_manager.manager import SoftwareManager + +# lkp's jitter parser emits the worst-case (99th percentile) instruction jitter +# under this key; it is the "maximum" jitter metric of interest. +_STAT_KEY = "jitter.Instantaneous_jitter_99th_percentile" + + +class Jitter(Test): + + """ + Intel LKP instruction jitter test. + + :avocado: enable + :avocado: tags=cpu,lkp,jitter,privileged + """ + + def setUp(self): + smm = SoftwareManager() + for package in ("make", "git", "gcc"): + if not smm.check_installed(package) and not smm.install(package): + self.cancel("%s is required to build lkp-tests." % package) + + self.run_timeout = self.params.get("timeout", default=3600) + self.threshold = self.params.get("jitter_threshold_cycles", default=0) + self.install_extra = self.params.get("lkp_install_extra", default="") + repo_url = self.params.get("lkp_repo_url") + if not repo_url: + self.cancel("lkp_repo_url must be set in the test parameters.") + repo_branch = self.params.get("lkp_repo_branch", default="master") + job = { + "suite": self.params.get("suite", default="jitter"), + "testcase": self.params.get("testcase", default="jitter"), + "jitter": { + "core_id": self.params.get("core_id", default=2), + "rate": self.params.get("rate", default=1000), + "loops": self.params.get("loops", default=10000), + "samples": self.params.get("samples", default=20), + }, + } + self.job_yaml = os.path.join(self.workdir, "jitter_job.yaml") + with open(self.job_yaml, "w", encoding="utf-8") as handle: + yaml.safe_dump(job, handle, default_flow_style=False) + + self.lkp_dir = lkp.clone( + repo_url, os.path.join(self.workdir, "lkp-tests"), + branch=repo_branch) + lkp.install(self.lkp_dir, extra=self.install_extra or None) + + self.testbox = socket.gethostname() + + def _read_max_jitter(self): + stats = lkp.find_result_file(self.lkp_dir, "stats.json") + if not stats: + self.fail("No stats.json produced by the jitter run.") + self.log.info("Reading jitter stats from %s", stats) + with open(stats, "r", encoding="utf-8") as handle: + data = json.load(handle) + value = data.get(_STAT_KEY) + if value is None: + self.fail("'%s' not found in %s" % (_STAT_KEY, stats)) + return float(value) + + def test(self): + subs = lkp.install_job( + self.lkp_dir, self.job_yaml, self.testbox) + for sub in subs: + result = lkp.run_job(self.lkp_dir, sub, timeout=self.run_timeout) + if result.exit_status != 0: + lkp.archive_results(self.lkp_dir, "stats.json", + os.path.join(self.outputdir, "lkp-results")) + self.fail("lkp run failed for %s (exit %s)" % + (os.path.basename(sub), result.exit_status)) + + lkp.archive_results(self.lkp_dir, "stats.json", + os.path.join(self.outputdir, "lkp-results")) + max_jitter = self._read_max_jitter() + self.log.info("Maximum instruction jitter: %s cycles", max_jitter) + if self.threshold > 0 and max_jitter > self.threshold: + self.fail("Instruction jitter %s cycles exceeds threshold %s" % + (max_jitter, self.threshold)) diff --git a/cpu/jitter.py.data/jitter.yaml b/cpu/jitter.py.data/jitter.yaml new file mode 100644 index 000000000..ad7186ba0 --- /dev/null +++ b/cpu/jitter.py.data/jitter.yaml @@ -0,0 +1,25 @@ +# Avocado test timeout (seconds). Also used as the per-step timeout for the +# lkp install/run helpers, so it must comfortably exceed the benchmark runtime. +timeout: 3600 + +# lkp-tests source. Override lkp_repo_url for a local mirror if github.com is +# unreachable from the test box. +lkp_repo_url: "https://github.com/intel/lkp-tests.git" +lkp_repo_branch: master + +# Extra args appended to the base "lkp install" run in setUp only (it does +# not affect the per sub-job "lkp install" performed by install_job, which is +# always run). +lkp_install_extra: "" + +# Fail the test when the measured maximum instruction jitter exceeds this many +# cycles. 0 disables the assertion (measure-and-report only). +jitter_threshold_cycles: 0 + +# LKP jitter job definition (read individually, assembled by the test). +suite: jitter +testcase: jitter +core_id: 2 +rate: 1000 +loops: 10000 +samples: 20 diff --git a/cpu/lkp_stressng_cpu.py b/cpu/lkp_stressng_cpu.py new file mode 100644 index 000000000..94870e530 --- /dev/null +++ b/cpu/lkp_stressng_cpu.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# See LICENSE for more details. +# +# Copyright: 2026 Advanced Micro Devices, Inc. +# Author: Sumit Kumar + +""" +Run the Intel LKP stress-ng CPU saturation microbenchmark under Avocado and +report how long it takes for the CPUs to reach a target busy percentage, +using the mpstat monitor samples collected during the run. +""" + +import json +import os +import socket + +import yaml + +from autils.devel import lkp +from avocado import Test +from avocado.utils.software_manager.manager import SoftwareManager + +# lkp's mpstat monitor aggregates utilisation across all CPUs under this key. +_IDLE_KEY = "mpstat.cpu.all.idle%" + + +class StressNgCpuLkp(Test): + + """ + Intel LKP stress-ng CPU saturation test. + + Run with a coherent job timeout, e.g.:: + + avocado run --job-timeout=600 cpu/stress_ng_cpu.py + + :avocado: enable + :avocado: tags=cpu,lkp,stress-ng,privileged + """ + + def setUp(self): + smm = SoftwareManager() + for package in ("make", "git", "gcc"): + if not smm.check_installed(package) and not smm.install(package): + self.cancel("%s is required to build lkp-tests." % package) + + self.run_timeout = self.params.get("timeout", default=3600) + self.saturation_pct = self.params.get("cpu_saturation_pct", + default=95) + self.max_seconds = self.params.get("cpu_saturation_max_seconds", + default=0) + self.install_extra = self.params.get("lkp_install_extra", default="") + repo_url = self.params.get("lkp_repo_url") + if not repo_url: + self.cancel("lkp_repo_url must be set in the test parameters.") + repo_branch = self.params.get("lkp_repo_branch", default="master") + job = { + "suite": self.params.get("suite", default="stress-ng"), + "testcase": self.params.get("testcase", default="stress-ng"), + "nr_threads": self.params.get("nr_threads", default="100%"), + "testtime": self.params.get("testtime", default=60), + "monitors": { + "mpstat": { + "interval": self.params.get("mpstat_interval", + default=1), + }, + }, + "stress-ng": { + "test": ["cpu"], + }, + } + self.job_yaml = os.path.join(self.workdir, "stress_ng_cpu_job.yaml") + with open(self.job_yaml, "w", encoding="utf-8") as handle: + yaml.safe_dump(job, handle, default_flow_style=False) + + self.lkp_dir = lkp.clone( + repo_url, os.path.join(self.workdir, "lkp-tests"), + branch=repo_branch) + lkp.install(self.lkp_dir, extra=self.install_extra or None) + + self.testbox = socket.gethostname() + + def _saturation_seconds(self): + mpstat = lkp.find_result_file(self.lkp_dir, "mpstat.json") + if not mpstat: + self.fail("No mpstat.json produced by the stress-ng run.") + self.log.info("Reading mpstat data from %s", mpstat) + with open(mpstat, "r", encoding="utf-8") as handle: + data = json.load(handle) + + idle = data.get(_IDLE_KEY) + if not idle: + self.fail("'%s' series not found in %s" % (_IDLE_KEY, mpstat)) + + for index, value in enumerate(idle): + if (100.0 - float(value)) >= self.saturation_pct: + return index + 1 + return None + + def test(self): + subs = lkp.install_job( + self.lkp_dir, self.job_yaml, self.testbox) + for sub in subs: + result = lkp.run_job(self.lkp_dir, sub, timeout=self.run_timeout) + if result.exit_status != 0: + lkp.archive_results(self.lkp_dir, "mpstat.json", + os.path.join(self.outputdir, "lkp-results")) + self.fail("lkp run failed for %s (exit %s)" % + (os.path.basename(sub), result.exit_status)) + + lkp.archive_results(self.lkp_dir, "mpstat.json", + os.path.join(self.outputdir, "lkp-results")) + seconds = self._saturation_seconds() + if seconds is None: + self.fail("CPUs never reached %s%% busy during the run." % + self.saturation_pct) + self.log.info("cpu_saturation_seconds: %s", seconds) + if self.max_seconds > 0 and seconds > self.max_seconds: + self.fail("CPU saturation took %ss, exceeds limit %ss" % + (seconds, self.max_seconds)) diff --git a/cpu/lkp_stressng_cpu.py.data/stressng_cpu.yaml b/cpu/lkp_stressng_cpu.py.data/stressng_cpu.yaml new file mode 100644 index 000000000..4770838ed --- /dev/null +++ b/cpu/lkp_stressng_cpu.py.data/stressng_cpu.yaml @@ -0,0 +1,29 @@ +# Avocado test timeout (seconds). Also used as the per-step timeout for the +# lkp install/run helpers, so it must comfortably exceed "testtime" below. +# When invoking avocado, pass a coherent job timeout, e.g. +# avocado run --job-timeout=600 cpu/stress_ng_cpu.py +timeout: 3600 + +# lkp-tests source. Override lkp_repo_url for a local mirror if github.com is +# unreachable from the test box. +lkp_repo_url: "https://github.com/intel/lkp-tests.git" +lkp_repo_branch: master + +# Extra args appended to the base "lkp install" run in setUp only (it does +# not affect the per sub-job "lkp install" performed by install_job, which is +# always run). +lkp_install_extra: "" + +# Target busy percentage every CPU must reach (busy = 100 - %idle). +cpu_saturation_pct: 95 + +# Fail when full-CPU saturation takes longer than this many seconds. +# 0 disables the assertion (measure-and-report only). +cpu_saturation_max_seconds: 0 + +# LKP stress-ng job definition (read individually, assembled by the test). +suite: stress-ng +testcase: stress-ng +nr_threads: "100%" +testtime: 60 +mpstat_interval: 1