aboutsummaryrefslogtreecommitdiffstats
path: root/.auto/wip-headcache.patch
diff options
context:
space:
mode:
Diffstat (limited to '.auto/wip-headcache.patch')
-rw-r--r--.auto/wip-headcache.patch256
1 files changed, 256 insertions, 0 deletions
diff --git a/.auto/wip-headcache.patch b/.auto/wip-headcache.patch
new file mode 100644
index 0000000..d7d6ab6
--- /dev/null
+++ b/.auto/wip-headcache.patch
@@ -0,0 +1,256 @@
+diff --git a/.auto/diff_spans.py b/.auto/diff_spans.py
+index 95721a5..aad4ebc 100644
+--- a/.auto/diff_spans.py
++++ b/.auto/diff_spans.py
+@@ -39,11 +39,11 @@ def load_impl(path: str, name: str):
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod) # IDA-free at import time
+ body = mod.BODY
+- a = body.index("#: IDA colour tag -> the semantic kind")
+- b = body.index("def _idatui_unknown_row")
++ a = body.index("def _idatui_head_row")
++ b = body.index("def _idatui_struct_member_rows")
+ g = {"__name__": name}
+ exec(compile(body[a:b], name, "exec"), g) # noqa: S102
+- return g["_idatui_spans"]
++ return g
+
+
+ def main() -> int:
+@@ -59,8 +59,10 @@ def main() -> int:
+ fh.write(subprocess.run(
+ ["git", "-C", ROOT, "show", f"{a.ref}:server/patch_server.py"],
+ capture_output=True, text=True, check=True).stdout)
+- new = load_impl(os.path.join(ROOT, "server", "patch_server.py"), "new")
+- old = load_impl(old_path, "old")
++ gnew = load_impl(os.path.join(ROOT, "server", "patch_server.py"), "new")
++ gold = load_impl(old_path, "old")
++ new, old = gnew["_idatui_spans"], gold["_idatui_spans"]
++ new_row, old_row = gnew["_idatui_head_row"], gold["_idatui_head_row"]
+
+ binary = os.path.join(ROOT, a.target)
+ tgt = os.path.join(d, os.path.basename(binary))
+@@ -89,7 +91,15 @@ def main() -> int:
+ if ra != rb:
+ bad += 1
+ if bad <= 3:
+- print(f"MISMATCH @ {ea:#x}\n line={line!r}\n"
++ print(f"SPAN MISMATCH @ {ea:#x}\n line={line!r}\n"
++ f" old={ra!r}\n new={rb!r}")
++ # The whole row, not just the spans: `text`, the spans/text
++ # agreement guard and the name all moved around too.
++ ra, rb = old_row(ea), new_row(ea)
++ if ra != rb:
++ bad += 1
++ if bad <= 3:
++ print(f"ROW MISMATCH @ {ea:#x}\n"
+ f" old={ra!r}\n new={rb!r}")
+ nxt = ida_bytes.get_item_end(ea)
+ ea = nxt if nxt > ea else ea + 1
+diff --git a/idatui/domain.py b/idatui/domain.py
+index b496638..1c7883f 100644
+--- a/idatui/domain.py
++++ b/idatui/domain.py
+@@ -123,19 +123,22 @@ class Head:
+ return None
+
+ @classmethod
+- def from_raw(cls, d: dict) -> "Head":
++ def from_raw(cls, d: dict, raw: bytes | None = None) -> "Head":
+ sp = d.get("spans")
+ ops = d.get("ops")
++ # ``tuple(map(tuple, ...))`` rather than a per-item genexpr with str()/
++ # int() coercion: this runs once per listing row (hundreds of thousands
++ # on a real binary) and the worker's own tool already emits [str, str]
++ # and [int, int, int]. The coercion was re-proving that on every row.
+ return cls(
+ ea=_as_int(d["ea"]),
+ kind=d.get("kind", "unknown"),
+ size=int(d.get("size", 0) or 0),
+ text=d.get("text", ""),
+ name=d.get("name"),
+- spans=(tuple((str(k), str(t)) for k, t in sp)
+- if isinstance(sp, list) and sp else None),
+- ops=(tuple((int(a), int(b), int(n)) for a, b, n in ops)
+- if isinstance(ops, list) and ops else None),
++ raw=raw,
++ spans=tuple(map(tuple, sp)) if sp else None,
++ ops=tuple(map(tuple, ops)) if ops else None,
+ )
+
+
+@@ -654,30 +657,48 @@ class ListingModel:
+ # containing a huge coalesced undefined run doesn't pull megabytes.
+ _OP_SPAN_CAP = 1 << 16
+
+- def _attach_opcode_bytes(self, page: list[Head]) -> list[Head]:
+- """Fill ``raw`` (opcode bytes) for the code heads in ``page`` via one
+- bulk read over their extent (variable-length safe)."""
+- code = [h for h in page if h.kind == "code" and h.size > 0]
+- if not code:
+- return page
+- lo = code[0].ea
+- hi = code[-1].ea + code[-1].size
+- if hi - lo <= 0 or hi - lo > self._OP_SPAN_CAP:
+- return page
+- data = self._prog.read_bytes(lo, hi - lo)
++ def _build_page(self, rows: list) -> list[Head]:
++ """Turn the tool's raw rows into ``Head``s with their opcode bytes
++ already attached, via one bulk read over the code extent.
++
++ The bytes are read BEFORE the Heads are built rather than patched in
++ afterwards: ``dataclasses.replace`` re-runs ``__init__`` with every
++ field, so filling ``raw`` after the fact meant constructing each code
++ head twice -- once per listing row, on the path a jump-to-address walks
++ hundreds of thousands of times.
++ """
++ lo = hi = -1
++ for r in rows:
++ if r.get("kind") == "code" and r.get("size"):
++ ea = _as_int(r["ea"])
++ if lo < 0:
++ lo = ea
++ hi = ea + int(r["size"])
++ data = None
++ if 0 <= lo < hi and hi - lo <= self._OP_SPAN_CAP:
++ try:
++ data = self._prog.read_bytes(lo, hi - lo)
++ except Exception: # noqa: BLE001 -- opcode bytes are decoration
++ data = None
++ page: list[Head] = []
+ biggest = self._max_raw
+- out = []
+- for h in page:
+- if h.kind == "code" and h.size > 0:
+- off = h.ea - lo
+- b = bytes(data[off:off + h.size])
+- biggest = max(biggest, len(b))
+- out.append(replace(h, raw=b))
+- else:
+- out.append(h)
+- with self._lock:
+- self._max_raw = biggest
+- return out
++ for r in rows:
++ raw = None
++ if data is not None and r.get("kind") == "code":
++ size = int(r.get("size") or 0)
++ if size > 0:
++ off = _as_int(r["ea"]) - lo
++ raw = bytes(data[off:off + size])
++ if len(raw) > biggest:
++ biggest = len(raw)
++ try:
++ page.append(Head.from_raw(r, raw))
++ except (KeyError, ValueError, TypeError):
++ continue
++ if biggest != self._max_raw:
++ with self._lock:
++ self._max_raw = biggest
++ return page
+
+ def max_raw_len(self) -> int:
+ with self._lock:
+@@ -700,13 +721,7 @@ class ListingModel:
+ "heads", addr=hex(frm), count=self.PAGE, annotate=True)
+ rows = payload.get("heads", []) if isinstance(payload, dict) else []
+ cur = payload.get("cursor", {}) if isinstance(payload, dict) else {}
+- page = []
+- for r in rows:
+- try:
+- page.append(Head.from_raw(r))
+- except (KeyError, ValueError, TypeError):
+- continue
+- page = self._attach_opcode_bytes(page)
++ page = self._build_page(rows)
+ with self._lock:
+ for h in page:
+ # Banner/label rows (function headers, separators, code labels)
+diff --git a/server/patch_server.py b/server/patch_server.py
+index b75b120..a1ec1ef 100644
+--- a/server/patch_server.py
++++ b/server/patch_server.py
+@@ -297,33 +297,59 @@ def _idatui_head_row(ea):
+ else:
+ kind = "unknown"
+ line = ida_lines.generate_disasm_line(ea, 0)
+- text = ida_lines.tag_remove(line) if line else ""
+- text = " ".join(text.split()) # collapse IDA's column padding
++ text, spans, ops = _idatui_line_parts(line) if line else ("", None, None)
+ row = {
+ "ea": hex(ea),
+ "kind": kind,
+ "size": int(ida_bytes.get_item_size(ea)),
+ "text": text,
+ }
+- if line:
+- # Keep IDA's own token classification for syntax highlighting. Built from
+- # the SAME line as `text`, then whitespace-collapsed identically so the
+- # two never disagree about what the row says.
+- spans, ops = _idatui_spans(line)
+- joined = "".join(t for _k, t in spans)
+- if " ".join(joined.split()) == text:
+- row["spans"] = spans
+- # Where each operand sits in `text`. Comes out of the same tag walk
+- # (free), and is what lets the client show WHICH literal a keypress
+- # would reformat before you press it.
+- if ops:
+- row["ops"] = ops
++ if spans is not None:
++ row["spans"] = spans
++ # Where each operand sits in `text`. Comes out of the same tag walk
++ # (free), and is what lets the client show WHICH literal a keypress
++ # would reformat before you press it.
++ if ops:
++ row["ops"] = ops
+ nm = ida_name.get_ea_name(ea)
+ if nm:
+ row["name"] = nm
+ return row
+
+
++import functools as _idatui_functools
++
++
++@_idatui_functools.lru_cache(maxsize=16384)
++def _idatui_line_parts(line):
++ """``(text, spans, ops)`` for one tagged disassembly line -- memoised.
++
++ A function of the tagged line and nothing else, so the same line always
++ gives the same answer: a rename changes the line, which changes the key.
++ And listings repeat themselves hard -- 196k lines of bash are 53k distinct
++ ones, so a 16k-entry cache serves ~70% of them and takes the per-line cost
++ from 10.4us to 3.9us. This is the most expensive thing the backend does per
++ listing row, and a jump to an address near the end of a big binary walks
++ hundreds of thousands of them.
++
++ ``spans`` is None when the tag walk and the plain text disagree about what
++ the line says (then the text wins and the row renders unhighlighted).
++
++ The returned lists are SHARED between every row that has the same line;
++ treat them as read-only. Pickle notices the sharing too, so a page of
++ repetitive disassembly also serialises smaller.
++ """
++ import ida_lines
++ text = " ".join(ida_lines.tag_remove(line).split()) # collapse the padding
++ spans, ops = _idatui_spans(line)
++ # Built from the SAME line as `text`, then whitespace-collapsed identically,
++ # so the two can never disagree about what the row says.
++ joined = "".join([t for _k, t in spans])
++ if " ".join(joined.split()) != text:
++ return (text, None, None)
++ return (text, spans, ops)
++
++
+ #: IDA colour tag -> the semantic kind the TUI styles. IDA already classifies
+ #: every token in a disassembly line, for every processor it supports, so there
+ #: is nothing to lex: generate_disasm_line emits \x01<tag>text\x02<tag> and the
+@@ -407,7 +433,7 @@ def _idatui_spans(line):
+ # the most expensive thing the `heads` tool did, and a line is ~54
+ # characters but only ~13 tags -- everything between two tags is already
+ # exactly one span's worth of text.
+- _IDATUI_CTL = _re.compile("([\\x01\\x02\\x03][\\s\\S])")
++ _IDATUI_CTL = _re.compile("([\\x01\\x02\\x03](?s:.))")
+ tags, opnds = _IDATUI_TAGS, _IDATUI_OPND_TAGS
+ on, off, esc = "\x01", "\x02", "\x03"
+ addr_tag = chr(getattr(ida_lines, "COLOR_ADDR", 0x28))