diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye/analyze.py')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/analyze.py | 157 |
1 files changed, 152 insertions, 5 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/analyze.py b/debug/risc-v-gsp-rm-vm/greeneye/analyze.py index 66429e9..1c7e930 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/analyze.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/analyze.py @@ -6,12 +6,14 @@ offline comparison between runs (bare-metal vs VFIO, old firmware vs new). """ from __future__ import annotations +import json import os import re from collections import Counter, defaultdict from .record import load_events, load_meta +_TRACE_KINDS = ("exec", "access", "read", "write") _ACCESS_RE = re.compile( r"Invalid (read|write) at addr (0x[0-9a-fA-F]+), size (\d+), region '([^']*)', reason: (\w+)" ) @@ -90,13 +92,56 @@ def describe_fault(fault: dict, bias: int = 0) -> str: return "\n".join(lines) +def _trace_kind(record: dict) -> str | None: + """Native/TCG trace records use ``t``; folded session events use ``type``.""" + return record.get("type") or record.get("t") + + +def _number(value, default: int = 0) -> int: + if isinstance(value, int): + return value + if isinstance(value, str): + try: + return int(value, 0) + except ValueError: + return default + return default + + +def load_trace(session_dir: str, meta: dict | None = None) -> list[dict]: + """Load the trace blob referenced by ``meta['trace_blob']`` (R4).""" + meta = meta if meta is not None else load_meta(session_dir) + rel = meta.get("trace_blob") + if not rel: + return [] + path = rel if os.path.isabs(rel) else os.path.join(session_dir, rel) + records: list[dict] = [] + try: + with open(path, "r", encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if not line: + continue + record = json.loads(line) + if _trace_kind(record) != "summary": + records.append(record) + except (OSError, ValueError): + return [] + return records + + def render_summary(session_dir: str, symbols=None, max_rows: int = 40) -> str: meta = load_meta(session_dir) events = load_events(session_dir) - return render_summary_from(meta, events, symbols=symbols, max_rows=max_rows) + return render_summary_from( + meta, events, symbols=symbols, max_rows=max_rows, trace=load_trace(session_dir, meta) + ) -def render_summary_from(meta: dict, events: list[dict], symbols=None, max_rows: int = 40) -> str: +def render_summary_from(meta: dict, events: list[dict], symbols=None, max_rows: int = 40, + trace: list[dict] | None = None) -> str: + if trace is None: + trace = [e for e in events if _trace_kind(e) in _TRACE_KINDS] lines: list[str] = [] lines.append(f"# green-eye session summary: {meta.get('name', '?')}") lines.append("") @@ -194,6 +239,79 @@ def render_summary_from(meta: dict, events: list[dict], symbols=None, max_rows: lines.append(f"| `{page:#x}` | {count} | `{sym.name if sym else ''}` |") lines.append("") + # Execution/access trace coverage (native R4 or folded TCG trace) + if trace: + def access_kind(record): + kind = _trace_kind(record) + if kind == "access": + return "write" if str(record.get("rw", "")).startswith("w") else "read" + return kind + + execs = [r for r in trace if access_kind(r) == "exec"] + reads = [r for r in trace if access_kind(r) == "read"] + writes = [r for r in trace if access_kind(r) == "write"] + lines.append("## Execution trace") + lines.append("") + blob = meta.get("trace_blob") + if blob: + lines.append(f"* trace blob: `{blob}`") + pcs = Counter() + for record in execs: + pcs[_number(record.get("pc"))] += 1 + suffix = f", unique PCs: {len(pcs)}" if execs else " (exec=off)" + lines.append(f"* exec events: {len(execs)}{suffix}") + read_pages = Counter() + for record in reads: + read_pages[(_number(record.get("addr")) >> 12) << 12] += 1 + write_pages = Counter() + for record in writes: + write_pages[(_number(record.get("addr")) >> 12) << 12] += 1 + lines.append( + f"* accesses: {len(reads)} reads ({len(read_pages)} pages), " + f"{len(writes)} writes ({len(write_pages)} pages)" + ) + config = meta.get("trace") or {} + if config: + bits = [f"exec={'on' if config.get('exec') else 'off'}"] + for key in ("start", "end"): + if config.get(key) is not None: + bits.append(f"{key}={config[key]:#x}" if isinstance(config[key], int) else f"{key}={config[key]}") + if config.get("max_events"): + bits.append(f"max_events={config['max_events']}") + if config.get("dropped"): + bits.append(f"dropped={config['dropped']}") + lines.append("* trace config: " + ", ".join(bits)) + lines.append("") + + if execs: + by_sym = defaultdict(lambda: [0, 0]) # unique PCs, exec count + for pc, count in pcs.items(): + sym = symbols.resolve(pc) if symbols else None + entry = by_sym[sym.name if sym else f"{pc:#x}"] + entry[0] += 1 + entry[1] += count + lines.append("### Unique PCs by symbol") + lines.append("") + lines.append("| unique PCs | execs | symbol |") + lines.append("|---:|---:|---|") + for name, (unique, count) in sorted(by_sym.items(), key=lambda kv: -kv[1][1])[:max_rows]: + lines.append(f"| {unique} | {count} | `{name}` |") + lines.append("") + + if read_pages or write_pages: + lines.append("### Accessed pages") + lines.append("") + lines.append("| page | reads | writes | symbol |") + lines.append("|---|---:|---:|---|") + for page in sorted(set(read_pages) | set(write_pages), + key=lambda p: -(read_pages.get(p, 0) + write_pages.get(p, 0)))[:max_rows]: + sym = symbols.resolve(page) if symbols else None + lines.append( + f"| `{page:#x}` | {read_pages.get(page, 0)} | {write_pages.get(page, 0)} " + f"| `{sym.name if sym else ''}` |" + ) + lines.append("") + # Coverage steps = [e for e in events if e.get("type") == "step"] if steps: @@ -277,9 +395,16 @@ def compare_sessions(a_dir: str, b_dir: str, symbols=None, max_rows: int = 40) - """Diff two recorded sessions at the symbolic event level (A/B tooling).""" a_meta, b_meta = load_meta(a_dir), load_meta(b_dir) a_events, b_events = load_events(a_dir), load_events(b_dir) + a_trace, b_trace = load_trace(a_dir, a_meta), load_trace(b_dir, b_meta) lines = [f"# green-eye session comparison", "", f"* A: {a_meta.get('name')} ({a_dir})", f"* B: {b_meta.get('name')} ({b_dir})", ""] - def profile(events): + def trace_kind(record: dict) -> str: + kind = _trace_kind(record) + if kind == "access": + return "write" if str(record.get("rw", "")).startswith("w") else "read" + return kind + + def profile(events, trace): bp = Counter() watch = Counter() writes = Counter() @@ -290,10 +415,13 @@ def compare_sessions(a_dir: str, b_dir: str, symbols=None, max_rows: int = 40) - watch[(event.get("addr"), event.get("kind"))] += 1 elif event.get("type") == "mem_write": writes[(event.get("addr", 0) >> 12) << 12] += 1 + for record in trace: + if trace_kind(record) == "write": + writes[(_number(record.get("addr")) >> 12) << 12] += 1 return bp, watch, writes - a_bp, a_watch, a_writes = profile(a_events) - b_bp, b_watch, b_writes = profile(b_events) + a_bp, a_watch, a_writes = profile(a_events, a_trace) + b_bp, b_watch, b_writes = profile(b_events, b_trace) lines.append("## Breakpoint profile delta (A vs B)") lines.append("") @@ -318,4 +446,23 @@ def compare_sessions(a_dir: str, b_dir: str, symbols=None, max_rows: int = 40) - for page in sorted(set(a_writes) | set(b_writes), key=lambda k: -(a_writes.get(k, 0) + b_writes.get(k, 0)))[:max_rows]: lines.append(f"| `{page:#x}` | {a_writes.get(page, 0)} | {b_writes.get(page, 0)} |") lines.append("") + + def trace_totals(trace): + totals = Counter() + for record in trace: + kind = trace_kind(record) + if kind in ("exec", "read", "write"): + totals[kind] += 1 + return totals + + a_totals, b_totals = trace_totals(a_trace), trace_totals(b_trace) + if a_trace or b_trace: + lines.append("## Trace stream totals (A vs B)") + lines.append("") + lines.append("| stream | A | B | delta |") + lines.append("|---|---:|---:|---:|") + for kind in ("exec", "read", "write"): + a_count, b_count = a_totals.get(kind, 0), b_totals.get(kind, 0) + lines.append(f"| {kind} | {a_count} | {b_count} | {b_count - a_count:+d} |") + lines.append("") return "\n".join(lines) + "\n" |
