summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/trace.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 10:58:21 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 10:58:21 -0400
commit002e3d7d13c414ea874abbee8d82803dd3a72fe5 (patch)
tree6627c12fa07f6a1cf73e5aaeb7390e698dd3a896 /debug/risc-v-gsp-rm-vm/sim/trace.py
parent2f249817df327f5dcccf0464ad517fa60d6071d3 (diff)
downloadopen-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.tar.gz
open-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.zip
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
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim/trace.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/trace.py145
1 files changed, 145 insertions, 0 deletions
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}\""