From 002e3d7d13c414ea874abbee8d82803dd3a72fe5 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Fri, 2 Oct 2026 10:58:21 -0400 Subject: debug: green-eye wave 3 - RM harness, local simulator, device map, MCP plugin, value-validation demos, CI pipeline, toolchain docs; add host RM tools rmtool/rmtop --- debug/risc-v-gsp-rm-vm/sim/trace.py | 145 ++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 debug/risc-v-gsp-rm-vm/sim/trace.py (limited to 'debug/risc-v-gsp-rm-vm/sim/trace.py') diff --git a/debug/risc-v-gsp-rm-vm/sim/trace.py b/debug/risc-v-gsp-rm-vm/sim/trace.py new file mode 100644 index 0000000..04373b1 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/trace.py @@ -0,0 +1,145 @@ +"""Native execution/access trace (roadmap R4): the in-process equivalent of +``qemu/greeneye_trace.c``. + +The QEMU TCG plugin records every translated instruction and every memory +access as JSONL. This module does the same from inside +:class:`sim.machine.Machine`, with the same knobs and line shape: + +* ``exec=on|off`` - emit one ``exec`` line per executed instruction; +* ``start=``/``end=`` - only instrument PCs in the inclusive range (matching + the plugin, which filters at translation time, so memory accesses are also + filtered by the *instruction* PC); +* a bounded ring budget - once full, further lines are counted as ``dropped`` + instead of growing without limit (deterministically). + +Lines are JSON objects in the TCG plugin's schema (``t``/``pc``/``addr``/ +``size``, hex strings), plus a ``vt`` virtual-time field (the machine's +instruction count). Nothing here reads the host clock, so the payload is +byte-identical for identical execution. +""" +from __future__ import annotations + +#: Default cap on buffered trace lines (~50 bytes each in memory). +DEFAULT_MAX_EVENTS = 1_000_000 + + +class Trace: + """Collects compact exec/access lines for one native session.""" + + def __init__(self, *, exec_enabled: bool = True, start: int | None = None, + end: int | None = None, max_events: int | None = None): + self.exec_enabled = bool(exec_enabled) + self.start = start + self.end = end + self.max_events = DEFAULT_MAX_EVENTS if max_events is None else max(0, int(max_events)) + self.execs = 0 + self.reads = 0 + self.writes = 0 + self.events = 0 + self.dropped = 0 + self._lines: list[str] = [] + self._flushed = False + + # -- configuration ----------------------------------------------------- + + def in_range(self, pc: int) -> bool: + if self.start is not None and pc < self.start: + return False + if self.end is not None and pc > self.end: + return False + return True + + def config(self) -> dict: + return { + "exec": self.exec_enabled, + "start": self.start, + "end": self.end, + "max_events": self.max_events, + } + + def counts(self) -> dict: + return { + "exec": self.execs, + "read": self.reads, + "write": self.writes, + "events": self.events, + "dropped": self.dropped, + } + + # -- collection (called from Machine.run's hot loop) -------------------- + + def begin(self, pc: int, vt: int) -> bool: + """Called once per executed instruction; returns access-capture.""" + if not self.in_range(pc): + return False + if self.exec_enabled: + self._append(f'{{"t":"exec","vcpu":0,"pc":"0x{pc:x}","vt":{vt}}}') + self.execs += 1 + return True + + def mem(self, pc: int, addr: int, size: int, is_write: bool, vt: int) -> None: + kind = "write" if is_write else "read" + if is_write: + self.writes += 1 + else: + self.reads += 1 + self._append( + f'{{"t":"{kind}","vcpu":0,"addr":"0x{addr:x}","size":{size},' + f'"pc":"0x{pc:x}","vt":{vt}}}' + ) + + def _append(self, line: str) -> None: + self.events += 1 + if self.max_events and len(self._lines) >= self.max_events: + self.dropped += 1 + return + self._lines.append(line) + + # -- output ------------------------------------------------------------ + + def payload(self) -> bytes: + """The complete JSONL document, including the plugin's summary line.""" + body = "\n".join(self._lines) + if body: + body += "\n" + summary = ( + f'{{"t":"summary","insns":{self.execs},"mem":{self.reads + self.writes},' + f'"events":{self.events},"dropped":{self.dropped},' + f'"exec":{str(self.exec_enabled).lower()},' + f'"start":{_json_addr(self.start)},"end":{_json_addr(self.end)}}}' + ) + return (body + summary + "\n").encode("utf-8") + + def flush(self, recorder) -> str | None: + """Write the trace to ``recorder`` as ``blobs/trace.jsonl`` (once).""" + if self._flushed: + return None + self._flushed = True + if recorder is None or getattr(recorder, "dir", None) is None: + return None + rel = recorder.blob("trace.jsonl", self.payload()) + recorder.meta["trace_blob"] = rel + recorder.meta["trace"] = {**self.config(), **self.counts()} + return rel + + +def make_trace(spec): + """Normalise a trace spec (None/False, True, dict or Trace) to a Trace.""" + if spec is None or spec is False: + return None + if isinstance(spec, Trace): + return spec + if spec is True: + return Trace() + if isinstance(spec, dict): + return Trace( + exec_enabled=spec.get("exec", spec.get("exec_enabled", True)), + start=spec.get("start"), + end=spec.get("end"), + max_events=spec.get("max_events", spec.get("max")), + ) + raise ValueError(f"trace spec must be None, bool, dict or Trace, not {type(spec).__name__}") + + +def _json_addr(value: int | None) -> str: + return "null" if value is None else f"\"0x{value:x}\"" -- cgit v1.2.3