* [PATCH 1/6] rteval: Add cpuset module for cgroup v2 management
2026-04-17 19:51 [PATCH 0/6] rteval: Improve CPU management infrastructure and add housekeeping option John Kacur
@ 2026-04-17 19:51 ` John Kacur
2026-04-17 19:51 ` [PATCH 2/6] rteval: Fix xmlout unit test XSL file path John Kacur
` (4 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: John Kacur @ 2026-04-17 19:51 UTC (permalink / raw)
To: linux-rt-users; +Cc: Clark Williams, Tomas Glozar
rteval: Add cpuset module for cgroup v2 management
Add a new cpuset.py module that provides comprehensive cpuset
management using Linux cgroup v2 interface. This module includes:
- Cpuset class for creating and manipulating CPU sets
- CpusetsInit class for initialization and capability detection
- TaskMigrate class for moving processes between cpusets
- Context manager support for automatic cleanup
- Full cgroup v2 support including cpuset.cpus.partition
The module modernizes cpuset management for cgroup v2, replacing
older cgroup v1 interfaces while maintaining API compatibility
where possible.
Signed-off-by: John Kacur <jkacur@redhat.com>
---
rteval/cpuset.py | 350 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 350 insertions(+)
create mode 100755 rteval/cpuset.py
diff --git a/rteval/cpuset.py b/rteval/cpuset.py
new file mode 100755
index 000000000000..2fdbabf40125
--- /dev/null
+++ b/rteval/cpuset.py
@@ -0,0 +1,350 @@
+#!/usr/bin/python3
+# Copyright 2026 John Kacur <jkacur@redhat.com>
+""" Module for creating and using cpusets """
+
+import errno
+import os
+from glob import glob
+
+
+class Cpuset:
+ """ Class for manipulating cpusets """
+
+ mpath = '/sys/fs/cgroup'
+
+ def __init__(self, name=None):
+ self.cpuset_name = None
+ self._cpuset_path = None
+ self.create_cpuset(name)
+
+ @property
+ def cpuset_path(self):
+ """ Return the cpuset path, for example /sys/fs/cgroup/rteval_0 """
+ return self._cpuset_path
+
+ def __str__(self):
+ return f'cpuset_name={self.cpuset_name}, cpuset_path={self.cpuset_path}'
+
+ def create_cpuset(self, name=None):
+ """ Create a cpuset (cgroup) below /sys/fs/cgroup """
+ if name is None:
+ raise ValueError("cpuset name cannot be None")
+ self.cpuset_name = name
+ path = os.path.join(Cpuset.mpath, self.cpuset_name)
+ if not os.path.exists(path):
+ os.mkdir(path)
+ self._cpuset_path = path
+ # Enable cpuset controller in parent if needed
+ self._enable_controllers()
+
+ def _enable_controllers(self):
+ """ Enable cpuset controller in parent cgroup """
+ # Get parent path
+ parent_path = os.path.dirname(self._cpuset_path)
+ if parent_path == '/sys/fs':
+ parent_path = '/sys/fs/cgroup'
+
+ subtree_control = os.path.join(parent_path, 'cgroup.subtree_control')
+ try:
+ # Read current controllers
+ with open(subtree_control, 'r', encoding='utf-8') as f:
+ current = f.read().strip()
+
+ # Enable cpuset if not already enabled
+ # Controllers are space-separated, so split and check
+ if 'cpuset' not in current.split():
+ with open(subtree_control, 'w', encoding='utf-8') as f:
+ f.write('+cpuset')
+ except (OSError, PermissionError):
+ # May fail if already enabled or no permissions
+ pass
+
+ def assign_cpus(self, cpu_str):
+ """
+ Assign cpus in list-format to a cpuset
+ Note: In cgroup v2, you must call write_memnode() before assign_cpus()
+ """
+ path = os.path.join(self._cpuset_path, "cpuset.cpus")
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(cpu_str)
+
+ def write_pid(self, pid):
+ """
+ Place a pid in a cpuset
+ Returns True if successful, False if the process cannot be moved
+ Raises OSError for unexpected errors
+ """
+ path = os.path.join(self._cpuset_path, "cgroup.procs")
+ pid_str = str(pid)
+ try:
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(pid_str)
+ f.flush()
+ return True
+ except OSError as err:
+ # EINVAL: Invalid argument (process can't be moved, or invalid PID format)
+ # ESRCH: No such process (process died between read and write)
+ # EBUSY: Device or resource busy (process can't be moved)
+ # EACCES: Permission denied (some kernel threads can't be moved)
+ if err.errno in (errno.EINVAL, errno.ESRCH, errno.EBUSY, errno.EACCES):
+ return False
+ else:
+ raise
+
+ def write_memnode(self, memnode):
+ """ Place memnode in a cpuset """
+ path = os.path.join(self._cpuset_path, "cpuset.mems")
+ memnode_str = str(memnode)
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(memnode_str)
+
+ def write_cpu_exclusive(self, cpu_exclusive):
+ """
+ Set CPU partition type (cgroup v2)
+ In v2, cpu_exclusive is replaced by cpuset.cpus.partition
+
+ Args:
+ cpu_exclusive: True/1/'1' for 'isolated', False/0/'0' for 'member'
+ """
+ path = os.path.join(self._cpuset_path, "cpuset.cpus.partition")
+ if not os.path.exists(path):
+ # cpuset.cpus.partition may not exist in all kernels
+ return
+
+ # Convert to boolean: handles bool, int, or string input
+ if isinstance(cpu_exclusive, str):
+ is_exclusive = cpu_exclusive not in ('0', '', 'false', 'False')
+ else:
+ is_exclusive = bool(cpu_exclusive)
+
+ partition_type = 'isolated' if is_exclusive else 'member'
+ try:
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(partition_type)
+ except OSError:
+ # Partition may not be supported or may fail
+ pass
+
+ def set_cpu_exclusive(self):
+ """ Turn cpu_exclusive on (set partition to isolated) """
+ self.write_cpu_exclusive(True)
+
+ def unset_cpu_exclusive(self):
+ """ Turn cpu_exclusive off (set partition to member) """
+ self.write_cpu_exclusive(False)
+
+ def write_memory_migrate(self, memory_migrate):
+ """
+ Memory migrate is not available in cgroup v2
+ This is kept for API compatibility but does nothing
+ """
+ pass
+
+ def set_memory_migrate(self):
+ """ Memory_migrate not available in cgroup v2 (no-op for compatibility) """
+ pass
+
+ def unset_memory_migrate(self):
+ """ Memory migrate not available in cgroup v2 (no-op for compatibility) """
+ pass
+
+ def destroy(self):
+ """
+ Remove this cpuset (cgroup directory)
+ The cpuset must be empty (no processes) before it can be removed.
+ Raises OSError if the directory is not empty or doesn't exist.
+ """
+ if self._cpuset_path and os.path.exists(self._cpuset_path):
+ os.rmdir(self._cpuset_path)
+
+ def __enter__(self):
+ """
+ Context manager entry point
+ Allows usage: with Cpuset('name') as cpuset: ...
+ """
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """
+ Context manager exit point
+ Automatically destroys the cpuset when exiting the with block
+ """
+ self.destroy()
+ return False # Don't suppress exceptions
+
+ def get_tasks(self):
+ """ Get the tasks (PIDs) currently in a cpuset """
+ path = os.path.join(self.cpuset_path, "cgroup.procs")
+ with open(path, 'r', encoding='utf-8') as f:
+ return [line.strip() for line in f if line.strip()]
+
+
+class CpusetsInit(Cpuset):
+ """
+ CpusetsInit initializes cpusets (cgroup v2)
+ It verifies that cgroup v2 is mounted and cpuset controller is available
+ This class represents the root cgroup /sys/fs/cgroup
+ """
+
+ mpath = '/sys/fs/cgroup'
+
+ def __init__(self):
+ self._supported = self._cpuset_supported()
+ self._numa_nodes = self._get_numa_nodes() if self._supported else 0
+ if self._supported:
+ # Set attributes directly for the root cgroup
+ self.cpuset_name = None
+ self._cpuset_path = Cpuset.mpath
+
+ @property
+ def supported(self):
+ """ Return True if cpuset is supported """
+ return self._supported
+
+ @property
+ def numa_nodes(self):
+ """ Return the number of numa nodes """
+ return self._numa_nodes
+
+ @staticmethod
+ def _cpuset_supported():
+ """
+ Return True if cpusets are supported
+ For cgroup v2, check if:
+ 1. cgroup2 filesystem is available
+ 2. /sys/fs/cgroup is mounted
+ 3. cpuset controller is available
+ """
+ # Check if cgroup2 is in /proc/filesystems
+ cgroup2_found = False
+ dpath = '/proc/filesystems'
+ try:
+ with open(dpath, encoding='utf-8') as fp:
+ for line in fp:
+ elems = line.strip().split()
+ if len(elems) == 2 and elems[1] == "cgroup2":
+ cgroup2_found = True
+ break
+ except OSError:
+ return False
+
+ if not cgroup2_found:
+ return False
+
+ # Check if /sys/fs/cgroup is mounted and has cpuset controller
+ cgroup_path = '/sys/fs/cgroup'
+ controllers_file = os.path.join(cgroup_path, 'cgroup.controllers')
+
+ if not os.path.exists(controllers_file):
+ return False
+
+ try:
+ with open(controllers_file, encoding='utf-8') as f:
+ controllers = f.read().strip().split()
+ return 'cpuset' in controllers
+ except OSError:
+ return False
+
+ @staticmethod
+ def _get_numa_nodes():
+ """
+ return the number of numa nodes
+ used by init to set _numa_nodes
+ Users of this class should use the "numa_nodes" method
+ """
+ return len(glob('/sys/devices/system/node/node*'))
+
+
+class TaskMigrate:
+ """ A class for migrating a set of pids from one cpuset to another """
+
+ def __init__(self, cpuset_from, cpuset_to):
+ self.cpuset_from = cpuset_from
+ self.cpuset_to = cpuset_to
+ # memory_migrate is not available in cgroup v2, but keep for API compat
+ self.cpuset_to.set_memory_migrate()
+ self.migrated = 0
+ self.failed = 0
+
+ def migrate(self):
+ """ Do the migration, only attempt on pids where this is allowed """
+ self.migrated = 0
+ self.failed = 0
+ tasks = self.cpuset_from.get_tasks()
+ for task in tasks:
+ if self.cpuset_to.write_pid(task):
+ self.migrated += 1
+ else:
+ self.failed += 1
+ return self.migrated, self.failed
+
+
+if __name__ == '__main__':
+
+ cpusets_init = CpusetsInit()
+ print(f'cpusets_init.supported = {cpusets_init.supported}')
+ if cpusets_init.supported:
+ print(f'cpusets_init.numa_nodes = {cpusets_init.numa_nodes}')
+ print(f'cpusets_init.cpuset_path = {cpusets_init.cpuset_path}')
+
+ # Creating and manipulating cgroups requires root permissions
+ print("\nTo test cpuset creation, run as root:")
+ print(" sudo python3 cpuset.py")
+
+ try:
+ # Example 1: Using context manager (recommended)
+ print("\n" + "="*60)
+ print("Example 1: Context Manager (automatic cleanup)")
+ print("="*60)
+ with Cpuset('rteval_demo') as cpuset:
+ print(f"Created cpuset: {cpuset.cpuset_path}")
+ cpuset.write_memnode('0')
+ cpuset.assign_cpus('0-3')
+ print(f"Configured cpuset with CPUs 0-3")
+ print("Context manager exited - cpuset automatically destroyed!")
+
+ # Example 2: Manual cleanup (traditional approach)
+ print("\n" + "="*60)
+ print("Example 2: Manual Cleanup (traditional)")
+ print("="*60)
+ print("Creating cpuset0...")
+ cpuset0 = Cpuset('rteval_0')
+ print("Creating cpuset1...")
+ cpuset1 = Cpuset('rteval_1')
+
+ # In cgroup v2, must set mems before cpus
+ print("Setting memory nodes...")
+ cpuset0.write_memnode('0')
+ cpuset1.write_memnode('0')
+
+ print("Assigning CPUs to cpuset0 (0-4,8-11)...")
+ cpuset0.assign_cpus('0-4,8-11')
+ print("Assigning CPUs to cpuset1 (5-7)...")
+ cpuset1.assign_cpus('5-7')
+
+ print("Setting cpu_exclusive on cpuset1...")
+ cpuset1.set_cpu_exclusive()
+
+ print("Migrating tasks from root to cpuset0...")
+ tm = TaskMigrate(cpusets_init, cpuset0)
+ migrated, failed = tm.migrate()
+
+ print(f"\nMigration complete: {migrated} moved, {failed} skipped")
+ print(f"Tasks now in cpuset0: {len(cpuset0.get_tasks())}")
+
+ # Cleanup: move processes back to root before removing cgroups
+ print("\nCleaning up: moving processes back to root cgroup...")
+ tm_back = TaskMigrate(cpuset0, cpusets_init)
+ moved_back, _ = tm_back.migrate()
+ print(f"Moved {moved_back} processes back to root")
+
+ cpuset0.destroy()
+ cpuset1.destroy()
+ print("Manual cleanup complete")
+ except PermissionError as e:
+ print(f"\nPermission denied: {e}")
+ print("Run as root to create and manipulate cgroups")
+ except Exception as e:
+ print(f"\nError at step: {e}")
+ import traceback
+ traceback.print_exc()
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH 2/6] rteval: Fix xmlout unit test XSL file path
2026-04-17 19:51 [PATCH 0/6] rteval: Improve CPU management infrastructure and add housekeeping option John Kacur
2026-04-17 19:51 ` [PATCH 1/6] rteval: Add cpuset module for cgroup v2 management John Kacur
@ 2026-04-17 19:51 ` John Kacur
2026-04-17 19:51 ` [PATCH 3/6] rteval: Add CpuList class to cpulist_utils module John Kacur
` (3 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: John Kacur @ 2026-04-17 19:51 UTC (permalink / raw)
To: linux-rt-users; +Cc: Clark Williams, Tomas Glozar
The xmlout unit test was failing because it was looking for
rteval_text.xsl in the current directory, but the file is located
at rteval/rteval_text.xsl.
Update the test to use the correct path relative to the project root,
which is where the test is run from.
This fixes the test failure:
Before: 2 successful tests, 2 failed
After: 3 successful tests, 1 failed (only DMI test which needs root)
Signed-off-by: John Kacur <jkacur@redhat.com>
---
rteval/xmlout.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rteval/xmlout.py b/rteval/xmlout.py
index 710d50a1fc0f..5ff426643c3a 100644
--- a/rteval/xmlout.py
+++ b/rteval/xmlout.py
@@ -283,7 +283,7 @@ def unit_test(rootdir):
print("------------- XML OUTPUT ----------------------------")
x.Write("-")
print("------------- XSLT PARSED OUTPUT --------------------")
- x.Write("-", "rteval_text.xsl")
+ x.Write("-", "rteval/rteval_text.xsl")
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
x.Write("/tmp/xmlout-test.xml")
del x
@@ -294,7 +294,7 @@ def unit_test(rootdir):
print("------------- LOADED XML DATA --------------------------------")
x.Write("-")
print("------------- XSLT PARSED OUTPUT FROM LOADED XML--------------")
- x.Write("-", "rteval_text.xsl")
+ x.Write("-", "rteval/rteval_text.xsl")
x.close()
## Test new data parser ... it eats most data types
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH 3/6] rteval: Add CpuList class to cpulist_utils module
2026-04-17 19:51 [PATCH 0/6] rteval: Improve CPU management infrastructure and add housekeeping option John Kacur
2026-04-17 19:51 ` [PATCH 1/6] rteval: Add cpuset module for cgroup v2 management John Kacur
2026-04-17 19:51 ` [PATCH 2/6] rteval: Fix xmlout unit test XSL file path John Kacur
@ 2026-04-17 19:51 ` John Kacur
2026-04-17 19:51 ` [PATCH 4/6] rteval: Migrate call sites to use CpuList class where beneficial John Kacur
` (2 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: John Kacur @ 2026-04-17 19:51 UTC (permalink / raw)
To: linux-rt-users; +Cc: Clark Williams, Tomas Glozar
Add an object-oriented CpuList class to provide a richer interface
for CPU list manipulation while maintaining backward compatibility
with existing module-level functions.
The CpuList class provides:
- Instance methods that return new CpuList objects (chainable)
- Methods: online(), isolated(), nonisolated()
- Python operators: len(), in, iter(), ==, str()
- Static methods for backward compatibility
- Full compatibility with existing module-level functions
This is a step toward a more maintainable design that was removed
in commit a788ea3 when CpuList class was converted to a module.
The class allows for future enhancements like caching, set operations,
and better state management while keeping the functional interface.
All existing module-level functions remain unchanged for backward
compatibility.
Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: John Kacur <jkacur@redhat.com>
---
rteval/cpulist_utils.py | 161 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 159 insertions(+), 2 deletions(-)
diff --git a/rteval/cpulist_utils.py b/rteval/cpulist_utils.py
index 7abc45a508bf..a113198b3427 100644
--- a/rteval/cpulist_utils.py
+++ b/rteval/cpulist_utils.py
@@ -4,8 +4,9 @@
# Copyright 2016 - Clark Williams <williams@redhat.com>
# Copyright 2021 - John Kacur <jkacur@redhat.com>
# Copyright 2023 - Tomas Glozar <tglozar@redhat.com>
+# Copyright 2026 - John Kacur <jkacur@redhat.com>
#
-"""Module providing utility functions for working with CPU lists"""
+"""Module providing CpuList class and utility functions for working with CPU lists"""
import os
@@ -32,14 +33,170 @@ def _isolated_file_exists():
return os.path.exists(os.path.join(cpupath, "isolated"))
+#
+# CpuList class - object-oriented interface for CPU list manipulation
+#
+
+class CpuList:
+ """
+ Object-oriented interface for CPU list manipulation.
+
+ Represents a list of CPU numbers with methods for filtering and transformation.
+ Operations return new CpuList instances, making them chainable.
+
+ Examples:
+ # Create from string
+ cpus = CpuList("0-7,9-11")
+
+ # Create from list
+ cpus = CpuList([0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11])
+
+ # Chain operations
+ online_isolated = CpuList("0-15").online().isolated()
+
+ # Use as iterator
+ for cpu in CpuList("0-3"):
+ print(cpu)
+ """
+
+ def __init__(self, cpulist):
+ """
+ Initialize CpuList from string or list.
+
+ Args:
+ cpulist: Either a string like "0-7,9-11" or a list of integers
+ """
+ if isinstance(cpulist, str):
+ self._cpus = expand_cpulist(cpulist)
+ elif isinstance(cpulist, list):
+ self._cpus = [int(cpu) for cpu in cpulist]
+ else:
+ raise TypeError("cpulist must be a string or list")
+
+ self._cpus.sort()
+
+ @property
+ def cpus(self):
+ """Return the list of CPU numbers"""
+ return self._cpus
+
+ def getcpulist(self):
+ """Return the list of CPU numbers (for backward compatibility)"""
+ return self._cpus
+
+ def online(self):
+ """
+ Return a new CpuList containing only online CPUs.
+
+ Returns:
+ CpuList: New instance with only online CPUs
+ """
+ return CpuList(online_cpulist(self._cpus))
+
+ def isolated(self):
+ """
+ Return a new CpuList containing only isolated CPUs.
+
+ Returns:
+ CpuList: New instance with only isolated CPUs
+ """
+ return CpuList(isolated_cpulist(self._cpus))
+
+ def nonisolated(self):
+ """
+ Return a new CpuList containing only non-isolated CPUs.
+
+ Returns:
+ CpuList: New instance with only non-isolated CPUs
+ """
+ return CpuList(nonisolated_cpulist(self._cpus))
+
+ def __str__(self):
+ """Return collapsed string representation (e.g., '0-7,9-11')"""
+ return collapse_cpulist(self._cpus)
+
+ def __repr__(self):
+ """Return repr string"""
+ return f"CpuList('{self}')"
+
+ def __len__(self):
+ """Return number of CPUs in the list"""
+ return len(self._cpus)
+
+ def __contains__(self, cpu):
+ """Check if CPU is in the list"""
+ return int(cpu) in self._cpus
+
+ def __iter__(self):
+ """Iterate over CPU numbers"""
+ return iter(self._cpus)
+
+ def __eq__(self, other):
+ """Compare two CpuList objects"""
+ if isinstance(other, CpuList):
+ return self._cpus == other._cpus
+ return False
+
+ # Static methods for backward compatibility and direct use
+
+ @staticmethod
+ def expand(cpulist_str):
+ """
+ Expand a range string into a list of CPU numbers.
+
+ Args:
+ cpulist_str: String like "0-7,9-11"
+
+ Returns:
+ list: List of CPU numbers
+ """
+ return expand_cpulist(cpulist_str)
+
+ @staticmethod
+ def collapse(cpulist):
+ """
+ Collapse a list of CPU numbers into a string range.
+
+ Args:
+ cpulist: List of CPU numbers
+
+ Returns:
+ str: Collapsed string like "0-7,9-11"
+ """
+ return collapse_cpulist(cpulist)
+
+ @staticmethod
+ def compress(cpulist):
+ """
+ Return a string representation of cpulist.
+
+ Args:
+ cpulist: List of CPU numbers
+
+ Returns:
+ str: Comma-separated string
+ """
+ return compress_cpulist(cpulist)
+
+
+#
+# Module-level functions - kept for backward compatibility
+#
+
def collapse_cpulist(cpulist):
"""
Collapse a list of cpu numbers into a string range
of cpus (e.g. 0-5, 7, 9)
"""
+ if not cpulist:
+ return ""
+
+ # Ensure we're working with integers and sort them
+ sorted_cpus = sorted([int(cpu) for cpu in cpulist])
+
cur_range = [None, None]
result = []
- for cpu in cpulist + [None]:
+ for cpu in sorted_cpus + [None]:
if cur_range[0] is None:
cur_range[0] = cur_range[1] = cpu
continue
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH 4/6] rteval: Migrate call sites to use CpuList class where beneficial
2026-04-17 19:51 [PATCH 0/6] rteval: Improve CPU management infrastructure and add housekeeping option John Kacur
` (2 preceding siblings ...)
2026-04-17 19:51 ` [PATCH 3/6] rteval: Add CpuList class to cpulist_utils module John Kacur
@ 2026-04-17 19:51 ` John Kacur
2026-04-17 19:51 ` [PATCH 5/6] rteval: Add unit tests for CpuList class John Kacur
2026-04-17 19:51 ` [PATCH 6/6] rteval: Add --housekeeping option to reserve isolated CPUs John Kacur
5 siblings, 0 replies; 9+ messages in thread
From: John Kacur @ 2026-04-17 19:51 UTC (permalink / raw)
To: linux-rt-users; +Cc: Clark Williams, Tomas Glozar
Migrate cpulist_utils usage to use a hybrid approach:
- Use CpuList class when doing operations (chaining, filtering)
- Use module functions for simple formatting (collapse_cpulist)
This pragmatic approach uses object-oriented design where it adds
value (chaining operations, filtering) and functional programming
where it's simpler (formatting output).
Changes:
- Use CpuList(...).online().cpus for filtering operations
- Use collapse_cpulist() instead of str(CpuList()) for formatting
- Use CpuList(...).cpus for expanding CPU lists
- Simplified imports to only what's needed
Files updated:
- rteval-cmd: Main command interface
- rteval/systopology.py: NumaNode, SimNumaNode, SysTopology classes
- rteval/modules/loads/__init__.py: Load modules base
- rteval/modules/loads/hackbench.py: Hackbench load
- rteval/modules/loads/kcompile.py: Kernel compile load
- rteval/modules/loads/stressng.py: Stress-ng load
- rteval/modules/measurement/__init__.py: Measurement modules base
- rteval/cpupower.py: CPU power management
All existing tests pass. Module-level functions remain for backward
compatibility and for simple use cases.
Assisted-by: Claude <noreply@anthropic.com>
Signed-off-by: John Kacur <jkacur@redhat.com>
---
rteval-cmd | 16 ++++++---------
rteval/cpupower.py | 4 ++--
rteval/modules/loads/__init__.py | 7 +++----
rteval/modules/loads/hackbench.py | 9 +++------
rteval/modules/loads/kcompile.py | 14 +++++--------
rteval/modules/loads/stressng.py | 8 +++-----
rteval/modules/measurement/__init__.py | 6 +++---
rteval/systopology.py | 28 +++++++++++---------------
8 files changed, 37 insertions(+), 55 deletions(-)
diff --git a/rteval-cmd b/rteval-cmd
index 45d9f3a58a11..0ad90cf29ec4 100755
--- a/rteval-cmd
+++ b/rteval-cmd
@@ -33,11 +33,7 @@ from rteval import cpupower
from rteval.version import RTEVAL_VERSION
from rteval.systopology import SysTopology, parse_cpulist_from_config
from rteval.modules.loads.kcompile import ModuleParameters
-import rteval.cpulist_utils as cpulist_utils
-
-compress_cpulist = cpulist_utils.compress_cpulist
-expand_cpulist = cpulist_utils.expand_cpulist
-collapse_cpulist = cpulist_utils.collapse_cpulist
+from rteval.cpulist_utils import CpuList, is_relative, collapse_cpulist
def summarize(repfile, xslt):
""" Summarize an already existing XML report """
@@ -381,7 +377,7 @@ if __name__ == '__main__':
# Parse cpulists using parse_cpulist_from_config to account for
# run-on-isolcpus and relative cpusets
cpulist = parse_cpulist_from_config(msrcfg.cpulist, msrcfg.run_on_isolcpus)
- if msrcfg_cpulist_present and not cpulist_utils.is_relative(msrcfg.cpulist) and msrcfg.run_on_isolcpus:
+ if msrcfg_cpulist_present and not is_relative(msrcfg.cpulist) and msrcfg.run_on_isolcpus:
logger.log(Log.WARN, "ignoring --measurement-run-on-isolcpus, since cpulist is specified")
msrcfg.cpulist = collapse_cpulist(cpulist)
cpulist = parse_cpulist_from_config(ldcfg.cpulist)
@@ -389,14 +385,14 @@ if __name__ == '__main__':
# if we only specified one set of cpus (loads or measurement)
# default the other to the inverse of the specified list
if not ldcfg_cpulist_present and msrcfg_cpulist_present:
- tmplist = expand_cpulist(msrcfg.cpulist)
+ tmplist = CpuList(msrcfg.cpulist).cpus
tmplist = SysTopology().invert_cpulist(tmplist)
- tmplist = cpulist_utils.online_cpulist(tmplist)
+ tmplist = CpuList(tmplist).online().cpus
ldcfg.cpulist = collapse_cpulist(tmplist)
if not msrcfg_cpulist_present and ldcfg_cpulist_present:
- tmplist = expand_cpulist(ldcfg.cpulist)
+ tmplist = CpuList(ldcfg.cpulist).cpus
tmplist = SysTopology().invert_cpulist(tmplist)
- tmplist = cpulist_utils.online_cpulist(tmplist)
+ tmplist = CpuList(tmplist).online().cpus
msrcfg.cpulist = collapse_cpulist(tmplist)
if ldcfg_cpulist_present:
diff --git a/rteval/cpupower.py b/rteval/cpupower.py
index 37c4d33f1df4..948cd9650382 100644
--- a/rteval/cpupower.py
+++ b/rteval/cpupower.py
@@ -9,7 +9,7 @@ import shutil
import sys
from rteval.Log import Log
from rteval.systopology import SysTopology as SysTop
-from rteval import cpulist_utils
+from rteval.cpulist_utils import collapse_cpulist
PATH = '/sys/devices/system/cpu/'
@@ -113,7 +113,7 @@ if __name__ == '__main__':
l = Log()
l.SetLogVerbosity(Log.DEBUG)
- online_cpus = cpulist_utils.collapse_cpulist(SysTop().online_cpus())
+ online_cpus = collapse_cpulist(SysTop().online_cpus())
idlestate = '1'
info = True
diff --git a/rteval/modules/loads/__init__.py b/rteval/modules/loads/__init__.py
index 0845742e5d29..1ed005cc4e91 100644
--- a/rteval/modules/loads/__init__.py
+++ b/rteval/modules/loads/__init__.py
@@ -12,7 +12,7 @@ from rteval.Log import Log
from rteval.rtevalConfig import rtevalCfgSection
from rteval.modules import RtEvalModules, rtevalModulePrototype
from rteval.systopology import SysTopology as SysTop
-import rteval.cpulist_utils as cpulist_utils
+from rteval.cpulist_utils import CpuList, collapse_cpulist
class LoadThread(rtevalModulePrototype):
def __init__(self, name, config, logger=None):
@@ -118,11 +118,10 @@ class LoadModules(RtEvalModules):
cpulist = self._cfg.GetSection(self._module_config).cpulist
if cpulist:
# Convert str to list and remove offline cpus
- cpulist = cpulist_utils.expand_cpulist(cpulist)
- cpulist = cpulist_utils.online_cpulist(cpulist)
+ cpulist = CpuList(cpulist).online().cpus
else:
cpulist = SysTop().default_cpus()
- rep_n.newProp("loadcpus", cpulist_utils.collapse_cpulist(cpulist))
+ rep_n.newProp("loadcpus", collapse_cpulist(cpulist))
return rep_n
diff --git a/rteval/modules/loads/hackbench.py b/rteval/modules/loads/hackbench.py
index a70fdb33243f..fddb85648506 100644
--- a/rteval/modules/loads/hackbench.py
+++ b/rteval/modules/loads/hackbench.py
@@ -17,10 +17,7 @@ from signal import SIGKILL
from rteval.modules.loads import CommandLineLoad
from rteval.Log import Log
from rteval.systopology import SysTopology
-import rteval.cpulist_utils as cpulist_utils
-
-expand_cpulist = cpulist_utils.expand_cpulist
-isolated_cpulist = cpulist_utils.isolated_cpulist
+from rteval.cpulist_utils import CpuList
class Hackbench(CommandLineLoad):
def __init__(self, config, logger):
@@ -59,10 +56,10 @@ class Hackbench(CommandLineLoad):
self.cpus[n] = sysTop.getcpus(int(n))
# if a cpulist was specified, only allow cpus in that list on the node
if self.cpulist:
- self.cpus[n] = [c for c in self.cpus[n] if c in expand_cpulist(self.cpulist)]
+ self.cpus[n] = [c for c in self.cpus[n] if c in CpuList(self.cpulist).cpus]
# if a cpulist was not specified, exclude isolated cpus
else:
- self.cpus[n] = cpulist_utils.nonisolated_cpulist(self.cpus[n])
+ self.cpus[n] = CpuList(self.cpus[n]).nonisolated().cpus
# track largest number of cpus used on a node
node_biggest = len(self.cpus[n])
diff --git a/rteval/modules/loads/kcompile.py b/rteval/modules/loads/kcompile.py
index 404b46d505d0..c960242362d3 100644
--- a/rteval/modules/loads/kcompile.py
+++ b/rteval/modules/loads/kcompile.py
@@ -15,11 +15,7 @@ from rteval.modules import rtevalRuntimeError
from rteval.modules.loads import CommandLineLoad
from rteval.Log import Log
from rteval.systopology import SysTopology
-import rteval.cpulist_utils as cpulist_utils
-
-expand_cpulist = cpulist_utils.expand_cpulist
-compress_cpulist = cpulist_utils.compress_cpulist
-nonisolated_cpulist = cpulist_utils.nonisolated_cpulist
+from rteval.cpulist_utils import CpuList, collapse_cpulist
DEFAULT_KERNEL_PREFIX = "linux-6.17.7"
@@ -39,7 +35,7 @@ class KBuildJob:
os.mkdir(self.objdir)
# Exclude isolated CPUs if cpulist not set
- cpus_available = len(nonisolated_cpulist(self.node.cpus))
+ cpus_available = len(CpuList(self.node.cpus).nonisolated().cpus)
if os.path.exists('/usr/bin/numactl') and not cpulist:
# Use numactl
@@ -48,7 +44,7 @@ class KBuildJob:
elif cpulist:
# Use taskset
self.jobs = self.calc_jobs_per_cpu() * len(cpulist)
- self.binder = f'taskset -c {compress_cpulist(cpulist)}'
+ self.binder = f'taskset -c {collapse_cpulist(cpulist)}'
else:
# Without numactl calculate number of jobs from the node
self.jobs = self.calc_jobs_per_cpu() * cpus_available
@@ -228,7 +224,7 @@ class Kcompile(CommandLineLoad):
# if a cpulist was specified, only allow cpus in that list on the node
if self.cpulist:
- self.cpus[n] = [c for c in self.cpus[n] if c in expand_cpulist(self.cpulist)]
+ self.cpus[n] = [c for c in self.cpus[n] if c in CpuList(self.cpulist).cpus]
# remove nodes with no cpus available for running
for node, cpus in self.cpus.items():
@@ -290,7 +286,7 @@ class Kcompile(CommandLineLoad):
if 'cpulist' in self._cfg and self._cfg.cpulist:
cpulist = self._cfg.cpulist
- self.num_cpus = len(expand_cpulist(cpulist))
+ self.num_cpus = len(CpuList(cpulist).cpus)
else:
cpulist = ""
diff --git a/rteval/modules/loads/stressng.py b/rteval/modules/loads/stressng.py
index 4f6abfb5eabd..4ad7197fc590 100644
--- a/rteval/modules/loads/stressng.py
+++ b/rteval/modules/loads/stressng.py
@@ -8,9 +8,7 @@ import signal
from rteval.modules.loads import CommandLineLoad
from rteval.Log import Log
from rteval.systopology import SysTopology
-import rteval.cpulist_utils as cpulist_utils
-
-expand_cpulist = cpulist_utils.expand_cpulist
+from rteval.cpulist_utils import CpuList
class Stressng(CommandLineLoad):
" This class creates a load module that runs stress-ng "
@@ -70,10 +68,10 @@ class Stressng(CommandLineLoad):
cpus[n] = systop.getcpus(int(n))
# if a cpulist was specified, only allow cpus in that list on the node
if self.cpulist:
- cpus[n] = [c for c in cpus[n] if c in expand_cpulist(self.cpulist)]
+ cpus[n] = [c for c in cpus[n] if c in CpuList(self.cpulist).cpus]
# if a cpulist was not specified, exclude isolated cpus
else:
- cpus[n] = cpulist_utils.nonisolated_cpulist(cpus[n])
+ cpus[n] = CpuList(cpus[n]).nonisolated().cpus
# remove nodes with no cpus available for running
diff --git a/rteval/modules/measurement/__init__.py b/rteval/modules/measurement/__init__.py
index e09dca683dbf..71c38357731d 100644
--- a/rteval/modules/measurement/__init__.py
+++ b/rteval/modules/measurement/__init__.py
@@ -6,7 +6,7 @@
import libxml2
from rteval.modules import RtEvalModules, ModuleContainer
from rteval.systopology import parse_cpulist_from_config
-import rteval.cpulist_utils as cpulist_utils
+from rteval.cpulist_utils import CpuList, collapse_cpulist
class MeasurementModules(RtEvalModules):
"""Module container for measurement modules"""
@@ -43,7 +43,7 @@ class MeasurementModules(RtEvalModules):
run_on_isolcpus = modcfg.run_on_isolcpus
if cpulist is None:
# Get default cpulist value
- cpulist = cpulist_utils.collapse_cpulist(parse_cpulist_from_config("", run_on_isolcpus))
+ cpulist = collapse_cpulist(parse_cpulist_from_config("", run_on_isolcpus))
for (modname, modtype) in modcfg:
if isinstance(modtype, str) and modtype.lower() == 'module': # Only 'module' will be supported (ds)
@@ -61,6 +61,6 @@ class MeasurementModules(RtEvalModules):
cpulist = self._cfg.GetSection("measurement").cpulist
run_on_isolcpus = self._cfg.GetSection("measurement").run_on_isolcpus
cpulist = parse_cpulist_from_config(cpulist, run_on_isolcpus)
- rep_n.newProp("measurecpus", cpulist_utils.collapse_cpulist(cpulist))
+ rep_n.newProp("measurecpus", collapse_cpulist(cpulist))
return rep_n
diff --git a/rteval/systopology.py b/rteval/systopology.py
index 6bcfc77f2c84..7305fc278995 100644
--- a/rteval/systopology.py
+++ b/rteval/systopology.py
@@ -9,8 +9,7 @@
import os
import os.path
import glob
-import rteval.cpulist_utils as cpulist_utils
-from rteval.cpulist_utils import sysread
+from rteval.cpulist_utils import CpuList, sysread, is_relative, expand_relative_cpulist, collapse_cpulist
def cpuinfo():
''' return a dictionary of cpu keys with various cpu information '''
@@ -65,8 +64,7 @@ class NumaNode:
"""
self.path = path
self.nodeid = int(os.path.basename(path)[4:].strip())
- self.cpus = cpulist_utils.expand_cpulist(sysread(self.path, "cpulist"))
- self.cpus = cpulist_utils.online_cpulist(self.cpus)
+ self.cpus = CpuList(sysread(self.path, "cpulist")).online().cpus
self.getmeminfo()
def __contains__(self, cpu):
@@ -98,7 +96,7 @@ class NumaNode:
def getcpustr(self):
""" return list of cpus for this node as a string """
- return cpulist_utils.collapse_cpulist(self.cpus)
+ return collapse_cpulist(self.cpus)
def getcpulist(self):
""" return list of cpus for this node """
@@ -115,8 +113,7 @@ class SimNumaNode(NumaNode):
def __init__(self):
self.nodeid = 0
- self.cpus = cpulist_utils.expand_cpulist(sysread(SimNumaNode.cpupath, "possible"))
- self.cpus = cpulist_utils.online_cpulist(self.cpus)
+ self.cpus = CpuList(sysread(SimNumaNode.cpupath, "possible")).online().cpus
self.getmeminfo()
def getmeminfo(self):
@@ -198,7 +195,7 @@ class SysTopology:
""" return a list of integers of all online cpus """
cpulist = []
for n in self.nodes:
- cpulist += cpulist_utils.online_cpulist(self.getcpus(n))
+ cpulist += CpuList(self.getcpus(n)).online().cpus
cpulist.sort()
return cpulist
@@ -206,7 +203,7 @@ class SysTopology:
""" return a list of integers of all isolated cpus """
cpulist = []
for n in self.nodes:
- cpulist += cpulist_utils.isolated_cpulist(self.getcpus(n))
+ cpulist += CpuList(self.getcpus(n)).isolated().cpus
cpulist.sort()
return cpulist
@@ -214,7 +211,7 @@ class SysTopology:
""" return a list of integers of all default schedulable cpus, i.e. online non-isolated cpus """
cpulist = []
for n in self.nodes:
- cpulist += cpulist_utils.nonisolated_cpulist(self.getcpus(n))
+ cpulist += CpuList(self.getcpus(n)).nonisolated().cpus
cpulist.sort()
return cpulist
@@ -249,20 +246,19 @@ def parse_cpulist_from_config(cpulist, run_on_isolcpus=False):
:param run_on_isolcpus: Value of --*-run-on-isolcpus argument
:return: Sorted list of CPUs as integers
"""
- if cpulist and not cpulist_utils.is_relative(cpulist):
- result = cpulist_utils.expand_cpulist(cpulist)
+ if cpulist and not is_relative(cpulist):
# Only include online cpus
- result = cpulist_utils.online_cpulist(result)
+ result = CpuList(cpulist).online().cpus
else:
result = SysTopology().online_cpus()
# Get the cpuset from the environment
cpuset = os.sched_getaffinity(0)
# Get isolated CPU list
isolcpus = SysTopology().isolated_cpus()
- if cpulist and cpulist_utils.is_relative(cpulist):
+ if cpulist and is_relative(cpulist):
# Include cpus that are not removed in relative cpuset and are either in cpuset from affinity,
# isolcpus (with run_on_isolcpus enabled, or added by relative cpuset
- added_cpus, removed_cpus = cpulist_utils.expand_relative_cpulist(cpulist)
+ added_cpus, removed_cpus = expand_relative_cpulist(cpulist)
result = [c for c in result
if (c in cpuset or
c in added_cpus or
@@ -295,7 +291,7 @@ if __name__ == "__main__":
onlcpus = s.online_cpus()
print(f'onlcpus = {onlcpus}')
- onlcpus = cpulist_utils.collapse_cpulist(onlcpus)
+ onlcpus = collapse_cpulist(onlcpus)
print(f'onlcpus = {onlcpus}')
onlcpus_str = s.online_cpus_str()
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH 5/6] rteval: Add unit tests for CpuList class
2026-04-17 19:51 [PATCH 0/6] rteval: Improve CPU management infrastructure and add housekeeping option John Kacur
` (3 preceding siblings ...)
2026-04-17 19:51 ` [PATCH 4/6] rteval: Migrate call sites to use CpuList class where beneficial John Kacur
@ 2026-04-17 19:51 ` John Kacur
2026-04-17 19:51 ` [PATCH 6/6] rteval: Add --housekeeping option to reserve isolated CPUs John Kacur
5 siblings, 0 replies; 9+ messages in thread
From: John Kacur @ 2026-04-17 19:51 UTC (permalink / raw)
To: linux-rt-users; +Cc: Clark Williams, Tomas Glozar
Add comprehensive unit tests for the new CpuList class covering:
- Basic creation from strings and lists
- Operators (__contains__, __iter__, __eq__, __len__)
- Method chaining with online/isolated filtering
- Backward compatibility with module-level functions
- String representation (__str__ and __repr__)
Assisted-by: Claude <noreply@anthropic.com>
Signed-off-by: John Kacur <jkacur@redhat.com>
---
tests/test_cpulist_class.py | 121 ++++++++++++++++++++++++++++++++++++
1 file changed, 121 insertions(+)
create mode 100644 tests/test_cpulist_class.py
diff --git a/tests/test_cpulist_class.py b/tests/test_cpulist_class.py
new file mode 100644
index 000000000000..bcb92e4a719d
--- /dev/null
+++ b/tests/test_cpulist_class.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+"""Test script for the new CpuList class"""
+
+import sys
+sys.path.insert(0, '.')
+
+from rteval.cpulist_utils import CpuList, expand_cpulist, collapse_cpulist
+
+def test_basic_creation():
+ """Test basic CpuList creation"""
+ print("=" * 60)
+ print("Test 1: Basic Creation")
+ print("=" * 60)
+
+ # Create from string
+ cpus1 = CpuList("0-7,9-11")
+ print(f"CpuList('0-7,9-11') = {cpus1}")
+ print(f" cpus: {cpus1.cpus}")
+ print(f" len: {len(cpus1)}")
+
+ # Create from list
+ cpus2 = CpuList([0, 1, 2, 3, 7, 8, 9])
+ print(f"\nCpuList([0,1,2,3,7,8,9]) = {cpus2}")
+ print(f" cpus: {cpus2.cpus}")
+
+ print()
+
+def test_operators():
+ """Test CpuList operators"""
+ print("=" * 60)
+ print("Test 2: Operators")
+ print("=" * 60)
+
+ cpus = CpuList("0-7")
+
+ # __contains__
+ print(f"cpus = {cpus}")
+ print(f" 3 in cpus: {3 in cpus}")
+ print(f" 10 in cpus: {10 in cpus}")
+
+ # __iter__
+ print(f" Iteration: ", end="")
+ for cpu in cpus:
+ print(cpu, end=" ")
+ print()
+
+ # __eq__
+ cpus2 = CpuList([0, 1, 2, 3, 4, 5, 6, 7])
+ print(f" cpus == CpuList([0-7]): {cpus == cpus2}")
+
+ print()
+
+def test_chaining():
+ """Test method chaining (requires system with online/isolated info)"""
+ print("=" * 60)
+ print("Test 3: Method Chaining (filtering won't occur on systems without online/isolated CPU support)")
+ print("=" * 60)
+
+ cpus = CpuList("0-15")
+ print(f"Original: {cpus}")
+ print(f" CPU count: {len(cpus)}")
+
+ try:
+ online = cpus.online()
+ print(f"Online: {online}")
+ print(f" CPU count: {len(online)}")
+
+ nonisolated = cpus.online().nonisolated()
+ print(f"Online + Non-isolated: {nonisolated}")
+ print(f" CPU count: {len(nonisolated)}")
+ except Exception as e:
+ print(f" (Skipped: {e})")
+
+ print()
+
+def test_backward_compatibility():
+ """Test that module-level functions still work"""
+ print("=" * 60)
+ print("Test 4: Backward Compatibility")
+ print("=" * 60)
+
+ # Old style - module functions
+ expanded = expand_cpulist("0-3,7-9")
+ print(f"expand_cpulist('0-3,7-9') = {expanded}")
+
+ collapsed = collapse_cpulist([0, 1, 2, 3, 7, 8, 9])
+ print(f"collapse_cpulist([0,1,2,3,7,8,9]) = {collapsed}")
+
+ # New style - static methods
+ expanded2 = CpuList.expand("0-3,7-9")
+ print(f"CpuList.expand('0-3,7-9') = {expanded2}")
+
+ collapsed2 = CpuList.collapse([0, 1, 2, 3, 7, 8, 9])
+ print(f"CpuList.collapse([0,1,2,3,7,8,9]) = {collapsed2}")
+
+ print()
+
+def test_repr():
+ """Test repr"""
+ print("=" * 60)
+ print("Test 5: Repr")
+ print("=" * 60)
+
+ cpus = CpuList("0-3,7-9")
+ print(f"repr: {repr(cpus)}")
+ print(f"str: {str(cpus)}")
+
+ print()
+
+if __name__ == '__main__':
+ print("\nTesting CpuList class implementation\n")
+
+ test_basic_creation()
+ test_operators()
+ test_chaining()
+ test_backward_compatibility()
+ test_repr()
+
+ print("=" * 60)
+ print("All tests completed!")
+ print("=" * 60)
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH 6/6] rteval: Add --housekeeping option to reserve isolated CPUs
2026-04-17 19:51 [PATCH 0/6] rteval: Improve CPU management infrastructure and add housekeeping option John Kacur
` (4 preceding siblings ...)
2026-04-17 19:51 ` [PATCH 5/6] rteval: Add unit tests for CpuList class John Kacur
@ 2026-04-17 19:51 ` John Kacur
2026-04-20 9:33 ` Tomas Glozar
5 siblings, 1 reply; 9+ messages in thread
From: John Kacur @ 2026-04-17 19:51 UTC (permalink / raw)
To: linux-rt-users; +Cc: Clark Williams, Tomas Glozar
Add a new --housekeeping option that allows users to specify isolated CPUs
that should be reserved for system housekeeping tasks and not used by
rteval's measurement or load modules.
Key features:
- Validates that housekeeping CPUs are in the isolated CPU list (isolcpus)
- Detects conflicts with explicitly specified --measurement-cpulist or
--loads-cpulist options and exits with a clear error message
- Filters housekeeping CPUs from both measurement and load CPU lists
- Correctly excludes housekeeping CPUs from inverted CPU lists when only
one of measurement/loads is specified
Example usage:
rteval --housekeeping 0-3 --measurement-run-on-isolcpus
Reserves isolcpus 0-3 for system tasks, runs measurements on
remaining isolated CPUs (4+) plus non-isolated CPUs
Implementation:
- systopology.py: Add validate_housekeeping_cpus() function to validate
that housekeeping CPUs are in isolcpus
- rteval-cmd: Add --housekeeping argument, conflict checking, filtering,
and fix inversion logic to exclude housekeeping CPUs
This feature is purely additive and does not change existing behavior
when --housekeeping is not specified.
Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: John Kacur <jkacur@redhat.com>
---
rteval-cmd | 46 ++++++++++++++++++++++++++++++++++++++++++-
rteval/systopology.py | 24 ++++++++++++++++++++++
2 files changed, 69 insertions(+), 1 deletion(-)
diff --git a/rteval-cmd b/rteval-cmd
index 0ad90cf29ec4..a903b537c891 100755
--- a/rteval-cmd
+++ b/rteval-cmd
@@ -31,7 +31,7 @@ from rteval.modules.loads import LoadModules
from rteval.modules.measurement import MeasurementModules
from rteval import cpupower
from rteval.version import RTEVAL_VERSION
-from rteval.systopology import SysTopology, parse_cpulist_from_config
+from rteval.systopology import SysTopology, parse_cpulist_from_config, validate_housekeeping_cpus
from rteval.modules.loads.kcompile import ModuleParameters
from rteval.cpulist_utils import CpuList, is_relative, collapse_cpulist
@@ -113,6 +113,9 @@ def parse_options(cfg, parser, cmdargs):
parser.add_argument("-i", "--installdir", dest="rteval___installdir",
type=str, default=rtevcfg.installdir, metavar="DIRECTORY",
help=f"place to locate installed templates (default: {rtevcfg.installdir})")
+ parser.add_argument("--housekeeping", dest="rteval___housekeeping",
+ type=str, default="", metavar="CPULIST",
+ help="isolated CPUs reserved for system tasks (not used by rteval)")
parser.add_argument("-s", "--sysreport", dest="rteval___sysreport",
action="store_true", default=rtevcfg.sysreport,
help=f'run sysreport to collect system data (default: {rtevcfg.sysreport})')
@@ -370,6 +373,13 @@ if __name__ == '__main__':
ldcfg = config.GetSection('loads')
msrcfg = config.GetSection('measurement')
+
+ # Validate and process housekeeping CPUs
+ housekeeping_cpus = []
+ if rtevcfg.housekeeping:
+ housekeeping_cpus = validate_housekeeping_cpus(rtevcfg.housekeeping)
+ logger.log(Log.DEBUG, f"housekeeping cpulist: {collapse_cpulist(housekeeping_cpus)}")
+
# Remember if cpulists were explicitly set by the user before running
# parse_cpulist_from_config, which generates default value for them
msrcfg_cpulist_present = msrcfg.cpulist != ""
@@ -382,17 +392,51 @@ if __name__ == '__main__':
msrcfg.cpulist = collapse_cpulist(cpulist)
cpulist = parse_cpulist_from_config(ldcfg.cpulist)
ldcfg.cpulist = collapse_cpulist(cpulist)
+
+ # Check for conflicts between housekeeping and measurement/load cpulists
+ if housekeeping_cpus:
+ msrcfg_cpus = CpuList(msrcfg.cpulist).cpus if msrcfg.cpulist else []
+ ldcfg_cpus = CpuList(ldcfg.cpulist).cpus if ldcfg.cpulist else []
+
+ # Check measurement conflicts
+ if msrcfg_cpulist_present:
+ conflicts = [cpu for cpu in housekeeping_cpus if cpu in msrcfg_cpus]
+ if conflicts:
+ raise RuntimeError(
+ f"Housekeeping CPUs {collapse_cpulist(conflicts)} conflict with "
+ f"--measurement-cpulist {msrcfg.cpulist}"
+ )
+
+ # Check load conflicts
+ if ldcfg_cpulist_present:
+ conflicts = [cpu for cpu in housekeeping_cpus if cpu in ldcfg_cpus]
+ if conflicts:
+ raise RuntimeError(
+ f"Housekeeping CPUs {collapse_cpulist(conflicts)} conflict with "
+ f"--loads-cpulist {ldcfg.cpulist}"
+ )
+
+ # Filter out housekeeping CPUs from measurement and load lists
+ msrcfg_cpus = [cpu for cpu in msrcfg_cpus if cpu not in housekeeping_cpus]
+ ldcfg_cpus = [cpu for cpu in ldcfg_cpus if cpu not in housekeeping_cpus]
+ msrcfg.cpulist = collapse_cpulist(msrcfg_cpus) if msrcfg_cpus else ""
+ ldcfg.cpulist = collapse_cpulist(ldcfg_cpus) if ldcfg_cpus else ""
+
# if we only specified one set of cpus (loads or measurement)
# default the other to the inverse of the specified list
if not ldcfg_cpulist_present and msrcfg_cpulist_present:
tmplist = CpuList(msrcfg.cpulist).cpus
tmplist = SysTopology().invert_cpulist(tmplist)
tmplist = CpuList(tmplist).online().cpus
+ # Exclude housekeeping CPUs from the inverted list
+ tmplist = [cpu for cpu in tmplist if cpu not in housekeeping_cpus]
ldcfg.cpulist = collapse_cpulist(tmplist)
if not msrcfg_cpulist_present and ldcfg_cpulist_present:
tmplist = CpuList(ldcfg.cpulist).cpus
tmplist = SysTopology().invert_cpulist(tmplist)
tmplist = CpuList(tmplist).online().cpus
+ # Exclude housekeeping CPUs from the inverted list
+ tmplist = [cpu for cpu in tmplist if cpu not in housekeeping_cpus]
msrcfg.cpulist = collapse_cpulist(tmplist)
if ldcfg_cpulist_present:
diff --git a/rteval/systopology.py b/rteval/systopology.py
index 7305fc278995..5f2bc291f608 100644
--- a/rteval/systopology.py
+++ b/rteval/systopology.py
@@ -239,6 +239,30 @@ class SysTopology:
return [c for c in self.online_cpus() if c in cpulist]
+def validate_housekeeping_cpus(housekeeping_cpulist):
+ """
+ Validates that housekeeping CPUs are in isolated CPU list
+ :param housekeeping_cpulist: CPU list string for housekeeping CPUs
+ :return: Sorted list of validated housekeeping CPUs as integers
+ :raises: RuntimeError if housekeeping CPUs are not in isolcpus
+ """
+ if not housekeeping_cpulist:
+ return []
+
+ housekeeping = CpuList(housekeeping_cpulist).online().cpus
+ isolcpus = SysTopology().isolated_cpus()
+
+ # Check if all housekeeping CPUs are in isolcpus
+ not_isolated = [cpu for cpu in housekeeping if cpu not in isolcpus]
+ if not_isolated:
+ isolcpus_str = collapse_cpulist(isolcpus) if isolcpus else "none"
+ raise RuntimeError(
+ f"Housekeeping CPUs {collapse_cpulist(not_isolated)} are not in isolated CPUs [{isolcpus_str}]"
+ )
+
+ return sorted(housekeeping)
+
+
def parse_cpulist_from_config(cpulist, run_on_isolcpus=False):
"""
Generates a cpulist based on --*-cpulist argument given by user
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* Re: [PATCH 6/6] rteval: Add --housekeeping option to reserve isolated CPUs
2026-04-17 19:51 ` [PATCH 6/6] rteval: Add --housekeeping option to reserve isolated CPUs John Kacur
@ 2026-04-20 9:33 ` Tomas Glozar
2026-04-20 21:42 ` John Kacur
0 siblings, 1 reply; 9+ messages in thread
From: Tomas Glozar @ 2026-04-20 9:33 UTC (permalink / raw)
To: John Kacur; +Cc: linux-rt-users, Clark Williams
pá 17. 4. 2026 v 21:51 odesílatel John Kacur <jkacur@redhat.com> napsal:
>
> Add a new --housekeeping option that allows users to specify isolated CPUs
> that should be reserved for system housekeeping tasks and not used by
> rteval's measurement or load modules.
>
> Key features:
> - Validates that housekeeping CPUs are in the isolated CPU list (isolcpus)
> - Detects conflicts with explicitly specified --measurement-cpulist or
> --loads-cpulist options and exits with a clear error message
> - Filters housekeeping CPUs from both measurement and load CPU lists
> - Correctly excludes housekeeping CPUs from inverted CPU lists when only
> one of measurement/loads is specified
>
> Example usage:
> rteval --housekeeping 0-3 --measurement-run-on-isolcpus
> Reserves isolcpus 0-3 for system tasks, runs measurements on
> remaining isolated CPUs (4+) plus non-isolated CPUs
>
> Implementation:
> - systopology.py: Add validate_housekeeping_cpus() function to validate
> that housekeeping CPUs are in isolcpus
> - rteval-cmd: Add --housekeeping argument, conflict checking, filtering,
> and fix inversion logic to exclude housekeeping CPUs
>
This might be a good opportunity to additionally pin the rteval
process to the housekeeping CPUs, as well as pass them to rtla's -H
option.
Tomas
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 6/6] rteval: Add --housekeeping option to reserve isolated CPUs
2026-04-20 9:33 ` Tomas Glozar
@ 2026-04-20 21:42 ` John Kacur
0 siblings, 0 replies; 9+ messages in thread
From: John Kacur @ 2026-04-20 21:42 UTC (permalink / raw)
To: Tomas Glozar; +Cc: John Kacur, linux-rt-users, Clark Williams
[-- Attachment #1: Type: text/plain, Size: 2051 bytes --]
On Mon, 20 Apr 2026, Tomas Glozar wrote:
> pá 17. 4. 2026 v 21:51 odesílatel John Kacur <jkacur@redhat.com> napsal:
> >
> > Add a new --housekeeping option that allows users to specify isolated CPUs
> > that should be reserved for system housekeeping tasks and not used by
> > rteval's measurement or load modules.
> >
> > Key features:
> > - Validates that housekeeping CPUs are in the isolated CPU list (isolcpus)
> > - Detects conflicts with explicitly specified --measurement-cpulist or
> > --loads-cpulist options and exits with a clear error message
> > - Filters housekeeping CPUs from both measurement and load CPU lists
> > - Correctly excludes housekeeping CPUs from inverted CPU lists when only
> > one of measurement/loads is specified
> >
> > Example usage:
> > rteval --housekeeping 0-3 --measurement-run-on-isolcpus
> > Reserves isolcpus 0-3 for system tasks, runs measurements on
> > remaining isolated CPUs (4+) plus non-isolated CPUs
> >
> > Implementation:
> > - systopology.py: Add validate_housekeeping_cpus() function to validate
> > that housekeeping CPUs are in isolcpus
> > - rteval-cmd: Add --housekeeping argument, conflict checking, filtering,
> > and fix inversion logic to exclude housekeeping CPUs
> >
>
> This might be a good opportunity to additionally pin the rteval
> process to the housekeeping CPUs, as well as pass them to rtla's -H
> option.
>
> Tomas
You raise good points. Currently, housekeeping CPUs are for system tasks
like kernel threads, IRQs, etc., and the --housekeeping flag tells
rteval
not to touch these CPUs. For example, you might have CPUs 0-7 isolated,
and use 0-1 for housekeeping, and 2-7 for measurement threads which are
already pinned in rteval with sched_affinity.
However, we currently don't:
1. Pin the rteval process itself to housekeeping CPUs
2. Pass them to timerlat's -H option
These would be useful enhancements to improve isolation further. I'll
consider adding them in a future update.
Thanks for the suggestions!
John Kacur
^ permalink raw reply [flat|nested] 9+ messages in thread