diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9717448 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM eci-base:latest + +# Add ECI repository key and sources +RUN wget -O- https://eci.intel.com/repos/gpg-keys/GPG-PUB-KEY-INTEL-ECI.gpg | tee /usr/share/keyrings/eci-archive-keyring.gpg > /dev/null + +RUN . /etc/os-release && \ + echo "deb [signed-by=/usr/share/keyrings/eci-archive-keyring.gpg] https://eci.intel.com/repos/${VERSION_CODENAME} isar main" | tee /etc/apt/sources.list.d/eci.list && \ + echo "deb-src [signed-by=/usr/share/keyrings/eci-archive-keyring.gpg] https://eci.intel.com/repos/${VERSION_CODENAME} isar main" | tee -a /etc/apt/sources.list.d/eci.list + +RUN bash -c 'echo -e "Package: *\nPin: origin eci.intel.com\nPin-Priority: 1000" > /etc/apt/preferences.d/isar' && \ + bash -c 'echo -e "\nPackage: libflann*\nPin: version 1.19.*\nPin-Priority: -1\n\nPackage: flann*\nPin: version 1.19.*\nPin-Priority: -1" >> /etc/apt/preferences.d/isar' + +RUN apt-get update && apt-get install -y --no-install-recommends \ + eci-realtime-benchmarking \ + rt-tests \ + intel-cmt-cat \ + git \ + && rm -rf /var/lib/apt/lists/* + +ENV USER=root +RUN chmod +x /opt/benchmarking/caterpillar/caterpillar + +# Install uv package manager +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" + +WORKDIR /app + +# Copy project files +COPY pyproject.toml uv.lock* ./ +COPY src/ ./src/ +COPY conf/ ./conf/ +COPY main.py ./ + +# Install Python 3.12 and sync dependencies +RUN uv python install 3.12 && uv sync + +# Default: run main.py with docker=false so benchmarks execute natively +ENTRYPOINT ["uv", "run", "python", "main.py", "run.docker=false", "pqos.enable=false", "run.stressor=false"] +# Override command to select benchmark, e.g.: +# docker run ... rt-tools-main:latest run.command=caterpillar +# docker run ... rt-tools-main:latest run.command=cyclictest diff --git a/README.md b/README.md index 245f89d..0384289 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ # rtos_bench: benchmarking suite to analyse real-time (RT) performance of an operating system -A lightweight, configurable Python framework for running system and application benchmarks using Hydra for flexible experiment management. -This repository provides a single entry-point script to run various performance tests with reproducible configurations defined in conf/config.yaml. +A comprehensive Python framework for benchmarking, analyzing and validating real-time (RT) performance of operating systems. Combines Docker-containerized benchmarks with statistical analysis tools based on Extreme Value Theory (EVT) to determine if a system meets real-time requirements. + +## Key Features + +- **Containerized Benchmarks**: Run reproducible RT benchmarks (Caterpillar, Cyclictest, iperf3, CODESYS) in Docker or on your host system +- **Intel RDT Integration**: Full support for Cache Allocation Technology (CAT) and Memory Bandwidth Allocation (MBA) +- **Statistical RT Validation**: EVT-based analysis with Region of Acceptance (RoA) for probabilistic WCET estimation +- **BIOS Collection via Redfish**: Automatically capture BIOS settings from BMC/iDRAC before benchmarks +- **Jupyter Analysis Notebooks**: Interactive reports for analyzing benchmark results and RT readiness ## Prerequisites: @@ -19,12 +26,32 @@ curl -LsSf https://astral.sh/uv/install.sh | sh uv sync ``` -## How to run benchmark +4. Additional system requirements: + - Docker (for containerized execution) + - `intel-cmt-cat` package (for pqos/Intel RDT support) + - Root access (required for pqos, IRQ affinity, and some metrics) + + + +## Quick Start ```bash -uv run main.py + +# Install dependencies and virtual environment (venv) +uv sync + +# Build all Docker images first +sudo .venv/bin/python3 main.py run.command=build + +# Run a benchmark (e.g. caterpillar) +sudo .venv/bin/python3 main.py run.command=caterpillar + +# Analyze results in Jupyter +uv run jupyter-lab ``` +> **Note**: Benchmarks require root access for pqos, IRQ affinity configuration, and hardware monitoring. Use `sudo .venv/bin/python3/main.py` instead of `uv run main.py` + ## How to run jupyter notebook (analysis software) ``` @@ -56,7 +83,6 @@ After that you can open any report and run it, just double-click on it like here ├── iperf3/ ├── mega-benchmark/ ├── codesys-jitter-benchmark/ -├── data/ # Store experiments here ├── outputs/ # Where we run experiment bundles ├── notebooks/ # Jupyter notebooks to analyse data ├── src/ # libraries @@ -73,24 +99,171 @@ All experiment parameters are controlled via Hydra’s configuration file at: conf/config.yaml ``` -## Example configuration +You can override any configuration parameter from the command line: +```bash +sudo .venv/bin/python3 main.py run.command=cyclictest run.t_core="3,5" ``` + +## Run Configuration + +```yaml run: - command: "caterpillar" - llc_cache_mask: "0x000f" - t_core: "3" - stressor: true - tests_path: "tests" + command: "caterpillar" # Benchmark to run + t_core: "9,11" # Target CPU cores + numa_node: "1" # NUMA node for cpuset-mems (should be same as NUMA node for t_core) + stressor: true # Enable stress workload + metrics: true # Enable metrics monitoring + docker: true # Run inside Docker container + cat_clos_pinning: + enable: true # Pin test PID to CLOS + clos: 1 # CLOS ID to use +``` + +| Parameter | Type | Description | +| ---------------------------- | ------- | -------------------------------------------------------------------------------------------------------------- | +| `run.command` | str | Benchmark to run: `caterpillar`, `cyclictest`, `iperf3`, `mega-benchmark`, `codesys-jitter-benchmark`, `codesys-opcua-pubsub`, or `build`. | +| `run.t_core` | str | Target CPU cores for running the benchmark (e.g., `"3,5,7,9"` or `"9,11"`) | +| `run.numa_node` | str | NUMA node for cpuset-mems (should be same as NUMA node for t_core) | +| `run.stressor` | bool | Enables additional stress workload during the benchmark | +| `run.metrics` | bool | Enable real-time metrics monitoring (CPU temp, IRQs, memory, etc.) | +| `run.docker` | bool | Run benchmark inside Docker container (if `false`, runs on host) | +| `run.cat_clos_pinning.enable`| bool | Enable pinning test PID to specified CLOS (caterpillar/cyclictest only) | +| `run.cat_clos_pinning.clos` | int | CLOS ID to pin the test process to | + +## Intel RDT/CAT Configuration (pqos) + +Configure Intel Resource Director Technology (Cache Allocation Technology, Memory Bandwidth Allocation): + +```yaml +pqos: + interface: "os" # 'os' for resctrl (recommended), 'msr' for direct access + reset_before_apply: true # Reset all allocations before applying new ones + + classes: + - id: 1 + description: "real-time workload" + l3_mask: "0x00ff" # L3 cache mask (8 cache ways) + l2_mask: "0x00ff" # L2 cache mask + mba: 100 # Memory Bandwidth Allocation (%) + pids: [] # PIDs to assign to this class + cores: [] # CPU cores to assign to this class + - id: 0 + description: "background worker" + l3_mask: "0x7f00" # Different cache ways for isolation + l2_mask: "0xff00" + mba: 10 + cores: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] + pids: [115, 118] ``` -| Parameter | Type | Description | -| ---------------- | ------- | -------------------------------------------------------------------------------------------------------------- | -| `command` | str | Benchmark to run. One of: `caterpillar`, `cyclictest`, `iperf3`, `mega-benchmark`, `codesys-jitter-benchmark`. | -| `llc_cache_mask` | str | Hexadecimal mask for Last-Level Cache (LLC) configuration. | -| `t_core` | str/int | Target CPU core for running the benchmark (i.e. '3,5,7,9') | -| `stressor` | bool | Enables additional stress workload during the benchmark. | -| `tests_path` | str | Path to the directory containing benchmark implementations. | +| Parameter | Type | Description | +| -------------------------- | ------ | ------------------------------------------------------------------ | +| `pqos.interface` | str | Interface mode: `os` (resctrl, required for PIDs) or `msr` (direct)| +| `pqos.reset_before_apply` | bool | Reset all allocations before applying new configuration | +| `pqos.classes[].id` | int | Class of Service (CLOS) ID | +| `pqos.classes[].l3_mask` | str | Hexadecimal L3 cache way mask | +| `pqos.classes[].l2_mask` | str | Hexadecimal L2 cache way mask | +| `pqos.classes[].mba` | int | Memory Bandwidth Allocation percentage (10-100) | +| `pqos.classes[].cores` | list | CPU cores to assign to this CLOS leave empty if not used | +| `pqos.classes[].pids` | list | Process IDs to assign to this CLOS leave empty if not used | + + +## IRQ Affinity Configuration + +Configure IRQ and RCU task affinity to isolate real-time cores: + +```yaml +irq_affinity: + enabled: true + housekeeping_cores: "0-1" # Cores for handling IRQs and RCU +``` + +## BIOS Settings Collection via Redfish + +Automatically collect BIOS settings from servers with Redfish-enabled BMC (e.g., Dell iDRAC) before running benchmarks: + +```yaml +bios: + enable: true + redfish: + host: "192.168.1.100" # BMC/iDRAC IP address + username: "root" + password: "YOUR_PASSWORD" + verify_ssl: false # Set to true for valid SSL certificates + timeout: 15 + + output: + format: "json" # Output format: json, yaml, or text + file: "${hydra:run.dir}/bios.json" + pretty: true +``` + +| Parameter | Type | Description | +| ------------------------- | ------ | ------------------------------------------------------------------ | +| `bios.enable` | bool | Enable/disable BIOS settings collection | +| `bios.redfish.host` | str | BMC/iDRAC hostname or IP address | +| `bios.redfish.username` | str | Username for Redfish API authentication | +| `bios.redfish.password` | str | Password for Redfish API authentication | +| `bios.redfish.verify_ssl` | bool | Verify SSL certificates (set `false` for self-signed certs) | +| `bios.redfish.timeout` | int | Connection timeout in seconds | +| `bios.output.format` | str | Output format: `json`, `yaml`, or `text` | +| `bios.output.file` | str | Path to save BIOS settings (supports Hydra interpolation) | +| `bios.output.pretty` | bool | Enable pretty-printing for JSON output | + +## Test-Specific Configuration + +### Caterpillar +```yaml +caterpillar: + n_cycles: 7200 # Number of measurement cycles +``` + +### Cyclictest +```yaml +cyclictest: + loops: 100000 # Number of test loops +``` + +## Metrics Monitoring + +When `run.metrics: true`, the following monitors collect data during benchmark execution: + +| Monitor | Output File | Description | +| ---------------- | ------------------------------ | ---------------------------------------- | +| CPU Monitor | `cpu_monitor.csv` | Per-core CPU temperatures | +| IRQ Monitor | `irq_monitor.csv` | Interrupt counts per CPU | +| MemInfo Monitor | `meminfo_monitor.csv` | Memory statistics from `/proc/meminfo` | +| SoftIRQ Monitor | `softirq_monitor.csv` | Software interrupt statistics | +| CPUStat Monitor | `cpustat_monitor.csv` | CPU usage statistics | +| PQOS Monitor | `pqos_monitor.csv` | Intel RDT monitoring data | + +Configure monitoring intervals in the config: + +```yaml +cpu_monitor: + path: "${hydra:run.dir}/cpu_monitor.csv" + interval: 1.0 +``` + +## Output Files + +Each benchmark run creates a timestamped directory in `outputs/` containing: + +- `output.csv` - Benchmark results +- `sysinfo.json` - System information snapshot (includes Hydra configuration) +- `bios.json` - BIOS settings (if enabled) +- `*_monitor.csv` - Various metrics logs (if enabled) +- `.hydra/` - Hydra configuration logs + +## Security Note +⚠️ **Important**: The Redfish password is stored in the configuration file. Consider: +- Using environment variables for sensitive credentials +- Restricting file permissions on `config.yaml` +- Not committing passwords to version control +## References +- [Dealing with Uncertainty in pWCET Estimations](https://dl.acm.org/doi/abs/10.1145/3396234) - Region of Acceptance methodology +- [Probabilistic-WCET Reliability](https://dl.acm.org/doi/10.1145/3126495) - EVT hypothesis validation diff --git a/conf/config.yaml b/conf/config.yaml index 3669971..eff241d 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -1,6 +1,22 @@ benchmark_output_path: "${hydra:run.dir}/output.csv" sysinfo_collector_file: "${hydra:run.dir}/sysinfo.json" +# --- Configuration for getting BIOS data from redfish + +bios: + enable: false + redfish: + host: "0.0.0.0" + username: "root" + password: "SECRET_PASSWORD" + verify_ssl: false + timeout: 15 + + output: + format: "json" + file: "${hydra:run.dir}/bios.json" + pretty: true + # --- Intel CAT specific configuration --- pqos: @@ -8,6 +24,7 @@ pqos: # 'msr' = Direct hardware access (Cores only, if some features are # not upstream kernel interface: "os" + enable: false # resets all allocations before applying new ones reset_before_apply: true @@ -17,7 +34,7 @@ pqos: description: "real-time workload" # Hexademical mask (e.g 0x003 means using the last 2 cache ways) l3_mask: "0x00ff" # comment if none - l2_mask: "0x00ff" + #l2_mask: "0x00ff" mba: 100 # set PIDs to assign to CLOS leave empty if none pids : [] @@ -27,26 +44,27 @@ pqos: description: "background worker" # Using different cache ways to isolate from class 1 l3_mask: "0x7f00" - l2_mask: "0xff00" + #l2_mask: "0xff00" mba: 10 cores: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] pids: [115, 118] irq_affinity: - enabled: true + enabled: false housekeeping_cores: "0-1" # --- Configuration related to tests running and parameters passed to it run: command: "caterpillar" t_core: "9,11" + max_count: 1 numa_node: "1" - stressor: true + stressor: false # tests_path: "tests" - metrics: true # enable metrics monitoring - docker: true # run inside docker container, if false will run on host system + metrics: false # enable metrics monitoring + docker: false # run inside docker container, if false will run on host system cat_clos_pinning: - enable: true # pins test PID to CLOS specified below works only for caterpillar and cyclictest + enable: false # pins test PID to CLOS specified below works only for caterpillar and cyclictest clos: 1 # --- Test specific configurations --- diff --git a/main.py b/main.py index 41f2b61..15f4926 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,8 @@ from omegaconf import DictConfig, OmegaConf +from src.bios_settings import process_bios_settings + from src.metrics import ( CPUmonitor, InterruptMonitor, @@ -18,6 +20,7 @@ from src.pqos_manager import PQOSManager from src.irq_affinity import set_irq_affinity from src.test_runner import DockerTestRunner +from scr.detect_cpus import detect_cpus def setup_pqos(cfg: DictConfig) -> None: @@ -75,28 +78,33 @@ def setup_metrics(cfg: DictConfig) -> None: cpustat_monitor = CpuStatMonitor( cfg.cpustat_monitor.path, cfg.softirq_monitor.interval ) - pqos_monitor = PQOSMonitor(cfg.pqos_monitor.path, cfg.pqos_monitor.interval) + if cfg.pqos.enable: + pqos_monitor = PQOSMonitor(cfg.pqos_monitor.path, cfg.pqos_monitor.interval) + pqos_monitor.start() cpu_monitor.start() interrupt_monitor.start() meminfo_monitor.start() softirq_monitor.start() cpustat_monitor.start() - pqos_monitor.start() -@hydra.main(version_base=None, config_path="conf", config_name="config") -def main(cfg: DictConfig): +def run_test(cfg: DictConfig): collector = SystemInfoCollector() collector.gather_all(cfg) collector.dump_to_file(cfg.sysinfo_collector_file) + # Collect BIOS settings via redfish + if cfg.bios.enable: + process_bios_settings(cfg.bios) + runner = DockerTestRunner(cfg) if cfg.run.command == "build": return runner.build() - setup_pqos(cfg) + if cfg.pqos.enable: + setup_pqos(cfg) # Handle test commands if cfg.run.command not in runner.tests: @@ -108,7 +116,76 @@ def main(cfg: DictConfig): if cfg.irq_affinity.enabled: set_irq_affinity(cfg.irq_affinity.housekeeping_cores) - return runner.run_test(cfg.run.command, cfg.run.t_core, cfg.run.stressor) + cores = detect_cpus() + if cores == "": + cores = cfg.run.t_core + + return runner.run_test(cfg.run.command, cores, cfg.run.stressor) + + +@hydra.main(version_base=None, config_path="conf", config_name="config") +def main(cfg: DictConfig): + execution_dir = os.getcwd() + counter_file = "/var/tmp/rt_tools_cur_count.txt" + service_name = "program-reboot.service" + service_path = f"/etc/systemd/system/{service_name}" + max_count = cfg.run.max_count + + if max_count <= 1: + print("max_count <=1. Running once and exiting.") + run_test(cfg) + sys.exit(0) + + cur_count = 0 + if os.path.exists(counter_file): + with open(counter_file, "r") as f: + cur_count = int(f.read().strip()) + else: + cur_count = 0 + + if cur_count == 0: + # First run: Setup systemd + print("First run (cur=0). Creating systemd service...") + service_content = f"""[Unit] +Description=Auto-run main.py on boot +After=network.target + +[Service] +Type=oneshot +User={os.getenv('USER')} +WorkingDirectory={execution_dir} +ExecStart=sudo ./env/python3 main.py +RemainAfterExit=no +Restart=no + +[Install] +WantedBy=multi-user.target +""" + with open(service_path, "w") as f: + f.write(service_content) + subprocess.run(["sudo", "systemctl", "daemon-reload"]) + subprocess.run(["sudo", "systemctl", "enable", service_name]) + + print(f"Run {cur_count + 1}/{max_count}") + run_test(cfg) + + # Increment and check + cur_count += 1 + with open(counter_file, "w") as f: + f.write(str(cur_count)) + + if cur_count >= max_count: + print("Max count reached. Cleaning up and exiting.") + if os.path.exists(service_path): + subprocess.run(["sudo", "systemctl", "stop", service_name], check=False) + subprocess.run(["sudo", "systemctl", "disable", service_name], check=False) + os.remove(service_path) + subprocess.run(["sudo", "systemctl", "daemon-reload"]) + os.remove(counter_file) + sys.exit(0) + else: + print("Rebooting for next run...") + os.system("sudo reboot") if __name__ == "__main__": diff --git a/src/bios_settings.py b/src/bios_settings.py new file mode 100644 index 0000000..7753cf2 --- /dev/null +++ b/src/bios_settings.py @@ -0,0 +1,260 @@ +import argparse +import urllib3 +import json +import re +import sys + +from omegaconf import DictConfig +from pathlib import Path +from typing import Dict, Optional, Tuple + + +try: + import requests + from requests.auth import HTTPBasicAuth + + REQUESTS_AVAILABLE = True +except ImportError: + print( + "ERROR: requests library not found. Install with: pip install requests", + file=sys.stderr, + ) + sys.exit(1) + +try: + import yaml + + YAML_AVAILABLE = True +except ImportError: + YAML_AVAILABLE = False + +# Disable SSL warnings for self-signed iDRAC certificates +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +import urllib3 + + +def process_bios_settings(cfg: DictConfig): + """ + Fetches BIOS settings using parameters from a Hydra config object. + + Expected Config Structure (example): + redfish: + enabled: true + host: "1.2.3.4" + username: "root" + password: "password" + verify_ssl: false + timeout: 10 + output: + format: "text" # json, yaml, text + file: "bios.json" # or null for stdout + pretty: true + """ + + # 1. Validation & Setup + # Access keys safely. Using .get() allows for defaults if keys are missing in YAML. + # We assume 'redfish' and 'output' are root keys in the passed cfg, + # or you can pass cfg.bios_task to this function. + rf_cfg = cfg.get("redfish", {}) + out_cfg = cfg.get("output", {}) + + if not rf_cfg.get("enabled", True): + print("WARNING: Redfish task is disabled in config", file=sys.stderr) + return + + host = rf_cfg.get("host") + username = rf_cfg.get("username", "root") + password = rf_cfg.get("password") + verify_ssl = rf_cfg.get("verify_ssl", False) + timeout = rf_cfg.get("timeout", 10) + + if not host: + print("ERROR: No host specified in config (redfish.host)", file=sys.stderr) + raise ValueError("Host Missing") + + if not password: + raise ValueError("ERROR: No password specified in config (redfish.password)") + + session, host_url = connect_redfish(host, username, password, verify_ssl, timeout) + if not session: + raise ValueError("Session missing") + + attributes = get_bios_attributes(session, host_url, timeout) + if not attributes: + raise ValueError("Attributes missing") + + output_fmt = out_cfg.get("format", "text") + is_pretty = out_cfg.get("pretty", True) + + if output_fmt == "json": + output_data = format_json(attributes, pretty=is_pretty) + elif output_fmt == "yaml": + output_data = format_yaml(attributes) + else: + output_data = format_text(attributes) + + output_file = out_cfg.get("file") + + if output_file: + try: + with open(output_file, "w") as f: + f.write(output_data) + print(f"✓ Saved to {output_file}", file=sys.stderr) + except Exception as e: + raise ValueError(f"ERROR: Failed to write to {output_file}: {e}") + else: + # Print to stdout + print(output_data) + + +def connect_redfish( + host: str, username: str, password: str, verify_ssl: bool = False, timeout: int = 10 +) -> Tuple[Optional[requests.Session], str]: + """ + Create authenticated Redfish session to iDRAC. + + Returns: + Tuple of (authenticated session or None, normalized host URL) + """ + # Ensure host has https:// prefix + if not host.startswith("http://") and not host.startswith("https://"): + host = f"https://{host}" + + session = requests.Session() + session.auth = HTTPBasicAuth(username, password) + session.verify = verify_ssl + session.headers.update( + {"Content-Type": "application/json", "Accept": "application/json"} + ) + + # Test connection with root endpoint + try: + print(f"Connecting to {host}...", file=sys.stderr) + response = session.get(f"{host}/redfish/v1/", timeout=timeout) + response.raise_for_status() + print(f"✓ Connected successfully", file=sys.stderr) + return session, host + except requests.exceptions.Timeout: + print(f"ERROR: Connection timeout to {host}", file=sys.stderr) + return None, host + except requests.exceptions.ConnectionError as e: + print(f"ERROR: Connection failed to {host}: {e}", file=sys.stderr) + return None, host + except requests.exceptions.HTTPError as e: + print(f"ERROR: HTTP error from {host}: {e}", file=sys.stderr) + return None, host + except Exception as e: + print(f"ERROR: Unexpected error connecting to {host}: {e}", file=sys.stderr) + return None, host + + +def get_bios_attributes( + session: requests.Session, host: str, timeout: int = 10 +) -> Optional[Dict[str, any]]: + """ + Retrieve BIOS attributes from Dell iDRAC. + + Returns: + Dictionary of BIOS attributes or None on failure + """ + try: + # Dell iDRAC standard endpoint for BIOS settings + url = f"{host}/redfish/v1/Systems/System.Embedded.1/Bios" + print(f"Fetching BIOS attributes from {url}...", file=sys.stderr) + + response = session.get(url, timeout=timeout) + response.raise_for_status() + data = response.json() + + # BIOS attributes are in the "Attributes" key + attributes = data.get("Attributes", {}) + print(f"✓ Retrieved {len(attributes)} BIOS attributes", file=sys.stderr) + return attributes + except requests.exceptions.HTTPError as e: + print(f"ERROR: Failed to fetch BIOS attributes: {e}", file=sys.stderr) + print( + f" Response: {e.response.text if e.response else 'No response'}", + file=sys.stderr, + ) + return None + except Exception as e: + print(f"ERROR: Unexpected error fetching BIOS: {e}", file=sys.stderr) + return None + + +def format_text(attributes: Dict[str, any]) -> str: + """ + Format BIOS attributes in human-readable text format. + Groups attributes by prefix for better organization. + """ + if not attributes: + return "No BIOS attributes available" + + lines = [] + lines.append("=" * 80) + lines.append(f"BIOS SETTINGS ({len(attributes)} total attributes)") + lines.append("=" * 80) + lines.append("") + + # Group by prefix (first word before capital letter) + groups = {} + ungrouped = [] + + for key in sorted(attributes.keys()): + value = attributes[key] + # Try to extract prefix (e.g., "Proc" from "ProcTurboMode") + match = re.match(r"^([A-Z][a-z]+)", key) + if match: + prefix = match.group(1) + if prefix not in groups: + groups[prefix] = [] + groups[prefix].append((key, value)) + else: + ungrouped.append((key, value)) + + # Output grouped attributes + for prefix in sorted(groups.keys()): + lines.append(f"[{prefix}*] Settings ({len(groups[prefix])} attributes)") + lines.append("-" * 80) + for key, value in groups[prefix]: + # Format value nicely + if isinstance(value, bool): + value_str = "Enabled" if value else "Disabled" + elif isinstance(value, str) and len(value) > 60: + value_str = value[:57] + "..." + else: + value_str = str(value) + lines.append(f" {key:<40} = {value_str}") + lines.append("") + + # Output ungrouped attributes + if ungrouped: + lines.append(f"[Other] Settings ({len(ungrouped)} attributes)") + lines.append("-" * 80) + for key, value in ungrouped: + if isinstance(value, bool): + value_str = "Enabled" if value else "Disabled" + elif isinstance(value, str) and len(value) > 60: + value_str = value[:57] + "..." + else: + value_str = str(value) + lines.append(f" {key:<40} = {value_str}") + lines.append("") + + return "\n".join(lines) + + +def format_json(attributes: Dict[str, any], pretty: bool = True) -> str: + """Format BIOS attributes as JSON.""" + if pretty: + return json.dumps(attributes, indent=2, sort_keys=True) + else: + return json.dumps(attributes, sort_keys=True) + + +def format_yaml(attributes: Dict[str, any]) -> str: + """Format BIOS attributes as YAML.""" + if not YAML_AVAILABLE: + raise ValueError("ERROR: PyYAML not installed. Cannot output YAML format.") + return yaml.dump(attributes, default_flow_style=False, sort_keys=True) diff --git a/src/detect_cpus.py b/src/detect_cpus.py new file mode 100644 index 0000000..9f02812 --- /dev/null +++ b/src/detect_cpus.py @@ -0,0 +1,56 @@ +from pathlib import Path +import os + + +def detect_cpus() -> str: + cpus = ( + _from_cgroup_v2() + or _from_proc_stat() + or _from_proc_cpuinfo() + or _from_sysconf() + ) + return str(cpus) if cpus else "" + + +def _from_cgroup_v2() -> int | None: + p = Path("/sys/fs/cgroup/cpuset.cpus.effective") + if not p.is_file(): + return None + total = 0 + for part in p.read_text().strip().split(","): + if "-" in part: + lo, hi = part.split("-", 1) + total += int(hi) - int(lo) + 1 + else: + total += 1 + return total or None + + +def _from_proc_stat() -> int | None: + p = Path("/proc/stat") + if not p.is_file(): + return None + count = sum( + 1 + for line in p.read_text().splitlines() + if line.startswith("cpu") and line[3:4].isdigit() + ) + return count or None + + +def _from_proc_cpuinfo() -> int | None: + p = Path("/proc/cpuinfo") + if not p.is_file(): + return None + count = sum( + 1 for line in p.read_text().splitlines() if line.startswith("processor") + ) + return count or None + + +def _from_sysconf() -> int | None: + n = os.sysconf("SC_NPROCESSORS_ONLN") if hasattr(os, "sysconf") else 0 + if n > 0: + return n + n = os.sysconf("SC_NPROCESSORS_CONF") if hasattr(os, "sysconf") else 0 + return n if n > 0 else None diff --git a/src/test_runner.py b/src/test_runner.py index 8d377c0..9c53d19 100644 --- a/src/test_runner.py +++ b/src/test_runner.py @@ -3,6 +3,7 @@ import subprocess import psutil import time +import shlex from typing import List, Optional from omegaconf import DictConfig, OmegaConf @@ -237,7 +238,7 @@ def _run_caterpillar(self, base_cmd: List[str], t_core: str, path: str) -> int: rdtset_cmd, ] else: - cmd = [caterpillar_cmd] + cmd = shlex.split(caterpillar_cmd) print(" ".join(cmd)) try: @@ -263,7 +264,10 @@ def _run_caterpillar(self, base_cmd: List[str], t_core: str, path: str) -> int: process.terminate() finally: try: - subprocess.run("docker stop $(docker ps -q)", shell=True, check=False) + if self.config.run.docker: + subprocess.run( + "docker stop $(docker ps -q)", shell=True, check=False + ) except Exception as e: print(f"Error stopping containers: {e}") @@ -276,7 +280,7 @@ def _run_cyclictest( cyclictest_cmd = ( # "chrt -r 95 " f"/usr/bin/cyclictest --threads -t 1 -p 95 " - f"-l {cycles} -d 1 -D 0 -i 100000 -a {t_core}" + f"-l {cycles} -d 1 -D 0 -i {self.config.caterpillar.n_cycles} -a {t_core}" ) if self.config.run.docker: rdtset_cmd = f"stdbuf -oL -eL " f"{cyclictest_cmd}" @@ -287,7 +291,7 @@ def _run_cyclictest( rdtset_cmd, ] else: - cmd = [cyclictest_cmd] + cmd = shlex.split(cyclictest_cmd) print(" ".join(cmd)) @@ -547,7 +551,7 @@ def _run_interactive_command(self, cmd: List[str]) -> Popen[str]: bufsize=1, ) - if self.config.run.cat_clos_pinning.enable: + if self.config.run.cat_clos_pinning.enable and self.config.pqos.enable: MAX_RETRIES = 5 SLEEP_TIME = 2 # Seconds to wait between tries