Files
retroSoC/scripts/run_flow.py
T
maksyuki 7592808464 feat: refactor JPEG table register bank and enhance flow script
- Updated the JPEG table register bank module to use combinational logic for data handling and simplified the write logic for quantization, DC, and AC entries.
- Introduced a new function in the run_flow script to handle process termination more gracefully, including signal handling for interrupts.
- Enhanced the flow script to log peak RSS memory usage during execution.
- Modified multiple testbench files to include new storage access signals and improved the test logic for better validation of KWS and SRAM interactions.
- Updated tests to validate the new peak RSS memory logging in the flow script.
- Adjusted Yosys synthesis recipes to include timeout settings and ensure proper reporting of pre-memory synthesis outputs.
2026-09-24 07:41:43 +08:00

231 lines
7.8 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import re
import resource
import signal
import subprocess
import sys
import termios
import time
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.setup_helpers import atomic_write # noqa: E402
FAILURE_MARKER = re.compile(r"(?:\bFAILED?\b|\bFATAL\b|assertion failed|%Error)", re.IGNORECASE)
def parse_env(values: list[str]) -> dict[str, str]:
environment = os.environ.copy()
for item in values:
name, separator, value = item.partition("=")
if not separator or not name:
raise ValueError(f"invalid environment assignment: {item}")
environment[name] = value
return environment
def should_display(tool: str, line: str) -> bool:
if tool != "yosys":
return True
keywords = ("Executing", "Warning", "Error", "Build succeeded", "End of script")
return any(keyword in line for keyword in keywords)
def jobserver_fds() -> tuple[int, ...]:
match = re.search(r"--jobserver-(?:auth|fds)=(\d+),(\d+)", os.environ.get("MAKEFLAGS", ""))
if not match:
return ()
descriptors = tuple(int(value) for value in match.groups())
try:
for descriptor in descriptors:
os.fstat(descriptor)
except OSError:
return ()
return descriptors
def terminal_requires_crlf() -> bool:
if not sys.stdout.isatty():
return False
try:
output_flags = termios.tcgetattr(sys.stdout.fileno())[1]
except OSError:
return False
return not (output_flags & termios.OPOST and output_flags & termios.ONLCR)
def write_console_output(line: str) -> None:
if terminal_requires_crlf():
line = line.replace("\n", "\r\n")
sys.stdout.write(line)
sys.stdout.flush()
def write_console_bytes(chunk: bytes) -> None:
if terminal_requires_crlf():
chunk = chunk.replace(b"\n", b"\r\n")
sys.stdout.buffer.write(chunk)
sys.stdout.buffer.flush()
def terminate_process_group(process: subprocess.Popen[str] | subprocess.Popen[bytes]) -> None:
if process.poll() is not None:
return
os.killpg(process.pid, signal.SIGTERM)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait()
def main() -> int:
parser = argparse.ArgumentParser(description="Run a flow command with structured results")
parser.add_argument("--tool", required=True)
parser.add_argument("--log", type=Path, required=True)
parser.add_argument("--result", type=Path, required=True)
parser.add_argument("--cwd", type=Path)
parser.add_argument("--env", action="append", default=[])
parser.add_argument(
"--stream-bytes",
action="store_true",
help="stream child output byte-by-byte and preserve raw log bytes",
)
parser.add_argument("--success-marker", help="marker used by an opt-in early-stop flow")
parser.add_argument(
"--terminate-on-success-marker",
action="store_true",
help="terminate the child successfully after --success-marker is logged",
)
parser.add_argument("command", nargs=argparse.REMAINDER)
args = parser.parse_args()
command = args.command[1:] if args.command[:1] == ["--"] else args.command
if not command:
parser.error("a command is required after --")
if args.terminate_on_success_marker and not args.success_marker:
parser.error("--terminate-on-success-marker requires --success-marker")
if args.terminate_on_success_marker and args.stream_bytes:
parser.error("success-marker termination is unsupported with --stream-bytes")
try:
environment = parse_env(args.env)
except ValueError as error:
parser.error(str(error))
args.log.parent.mkdir(parents=True, exist_ok=True)
args.result.parent.mkdir(parents=True, exist_ok=True)
started = datetime.now(timezone.utc)
start_clock = time.monotonic()
atomic_write(
args.result,
json.dumps(
{
"schema_version": 1,
"tool": args.tool,
"command": command,
"started_at": started.isoformat(),
"status": "running",
"log": str(args.log.resolve()),
},
indent=2,
sort_keys=True,
)
+ "\n",
)
def interrupt_flow(_signum: int, _frame: object) -> None:
raise KeyboardInterrupt
signal.signal(signal.SIGINT, interrupt_flow)
signal.signal(signal.SIGTERM, interrupt_flow)
returncode = 127
error_message = None
process: subprocess.Popen[str] | subprocess.Popen[bytes] | None = None
marker_seen = False
rejected_matches: list[str] = []
try:
if args.stream_bytes:
log_context = args.log.open("wb")
else:
log_context = args.log.open("w", encoding="utf-8", errors="replace")
with log_context as log:
process = subprocess.Popen(
command,
cwd=args.cwd,
env=environment,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=not args.stream_bytes,
errors=None if args.stream_bytes else "replace",
bufsize=0 if args.stream_bytes else 1,
pass_fds=jobserver_fds(),
start_new_session=True,
)
assert process.stdout is not None
if args.stream_bytes:
while chunk := os.read(process.stdout.fileno(), 4096):
log.write(chunk)
log.flush()
write_console_bytes(chunk)
else:
for line in process.stdout:
log.write(line)
log.flush()
match = FAILURE_MARKER.search(line)
if match:
rejected_matches.append(match.group(0))
if should_display(args.tool, line):
write_console_output(line)
if args.terminate_on_success_marker and args.success_marker in line:
marker_seen = True
terminate_process_group(process)
returncode = 1 if rejected_matches else 0
break
if not marker_seen:
returncode = process.wait()
except KeyboardInterrupt:
error_message = "interrupted"
if process is not None:
terminate_process_group(process)
returncode = 130
except (OSError, subprocess.SubprocessError) as error:
error_message = str(error)
finished = datetime.now(timezone.utc)
result = {
"schema_version": 1,
"tool": args.tool,
"command": command,
"cwd": str(args.cwd.resolve()) if args.cwd else os.getcwd(),
"started_at": started.isoformat(),
"finished_at": finished.isoformat(),
"duration_seconds": round(time.monotonic() - start_clock, 3),
"peak_rss_kib": resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss,
"exit_code": returncode,
"status": "passed" if returncode == 0 else "failed",
"log": str(args.log.resolve()),
}
if error_message:
result["error"] = error_message
if args.success_marker:
result["success_marker"] = args.success_marker
result["success_marker_seen"] = marker_seen
if marker_seen:
result["completion_mode"] = "success_marker"
if rejected_matches:
result["rejected_matches"] = sorted(set(rejected_matches))
atomic_write(args.result, json.dumps(result, indent=2, sort_keys=True) + "\n")
return returncode
if __name__ == "__main__":
raise SystemExit(main())