aboutsummaryrefslogtreecommitdiffstats
path: root/.auto/wip-digest.patch
diff options
context:
space:
mode:
Diffstat (limited to '.auto/wip-digest.patch')
-rw-r--r--.auto/wip-digest.patch162
1 files changed, 162 insertions, 0 deletions
diff --git a/.auto/wip-digest.patch b/.auto/wip-digest.patch
new file mode 100644
index 0000000..d4d23e8
--- /dev/null
+++ b/.auto/wip-digest.patch
@@ -0,0 +1,162 @@
+diff --git a/idatui/domain.py b/idatui/domain.py
+index 1f8263d..9cf539f 100644
+--- a/idatui/domain.py
++++ b/idatui/domain.py
+@@ -673,6 +673,14 @@ class ListingModel:
+ #: Whether a rename has ever staled this model. Until one has, every
+ #: read takes exactly the path it always did.
+ self._renamed = False
++ #: Where each loaded page starts, so a stale-text refresh can ask the
++ #: worker "is this page still what I have?" over exactly the extent the
++ #: worker itself produced. Parallel lists: first head index, the address
++ #: it was fetched from, the digest it came back with, and its row count.
++ self._page_head: list[int] = []
++ self._page_addr: list[int] = []
++ self._page_digest: list[object] = []
++ self._page_rows: list[int] = []
+ #: Set if a text refresh came back with a different head sequence, which
+ #: means something DID move the walk. Program.listing() throws the model
+ #: away when it sees this, so the next read rebuilds from scratch.
+@@ -758,6 +766,11 @@ class ListingModel:
+ page = self._build_page(rows)
+ with self._lock:
+ gen = self._text_gen
++ self._page_head.append(len(self._heads))
++ self._page_addr.append(frm)
++ self._page_digest.append(payload.get("digest")
++ if isinstance(payload, dict) else None)
++ self._page_rows.append(len(rows))
+ for h in page:
+ # Banner/label rows (function headers, separators, code labels)
+ # are display-only; don't index them so navigation lands on the
+@@ -924,13 +937,65 @@ class ListingModel:
+ search body asks for thousands at once) would come back short, fail the
+ sequence check, and condemn the model to a rebuild it did not need.
+ """
+- blk = self.TEXT_BLOCK
+ with self._lock:
+ n = len(self._heads)
+- start = (max(j0, 0) // blk) * blk
+- while start < min(j1, n):
+- self._ensure_text_block(start, min(start + blk, n))
+- start += blk
++ j0 = max(j0, 0)
++ j1 = min(j1, n)
++ while j0 < j1:
++ j0 = self._ensure_text_from(j0, j1, n)
++
++ def _ensure_text_from(self, j0: int, j1: int, n: int) -> int:
++ """Freshen from head ``j0`` and return where to carry on.
++
++ Tries the page ``j0`` falls in first: the worker can say whether that
++ page still renders exactly as it did, for the cost of the render alone
++ -- no rows on the wire, none unpickled, no Heads rebuilt. After a rename
++ nearly every page comes back identical, and that is 40% of what asking
++ for it again would have cost. Falls back to the block refetch when the
++ page has genuinely changed (or predates the digest).
++ """
++ with self._lock:
++ p = bisect.bisect_right(self._page_head, j0) - 1
++ usable = (0 <= p < len(self._page_head)
++ and self._page_digest[p] is not None)
++ if usable:
++ p_lo = self._page_head[p]
++ p_hi = (self._page_head[p + 1] if p + 1 < len(self._page_head)
++ else len(self._heads))
++ gen = self._text_gen
++ fresh = all(self._head_gen[k] == gen for k in range(p_lo, p_hi))
++ addr, want_dig = self._page_addr[p], self._page_digest[p]
++ want_rows = self._page_rows[p]
++ if usable and fresh:
++ return p_hi
++ if usable and self._verify_page(p, p_lo, p_hi, addr, want_dig,
++ want_rows, gen):
++ return p_hi
++ blk = self.TEXT_BLOCK
++ end = min(j0 + blk, j1 if usable else n)
++ self._ensure_text_block(j0, max(end, j0 + 1))
++ return max(end, j0 + 1)
++
++ def _verify_page(self, p: int, p_lo: int, p_hi: int, addr: int,
++ want_dig: object, want_rows: int, gen: int) -> bool:
++ """Ask whether page ``p`` still renders as it did; mark it fresh if so."""
++ try:
++ payload = self._prog.client.call(
++ "heads", addr=hex(addr), count=self.PAGE, annotate=True,
++ digest=True)
++ except Exception: # noqa: BLE001 -- an older worker has no digest mode
++ return False
++ if not isinstance(payload, dict):
++ return False
++ got = payload.get("digest")
++ if got is None or got != want_dig or payload.get("count") != want_rows:
++ return False
++ with self._lock:
++ if self._text_gen != gen or len(self._heads) < p_hi:
++ return False
++ for k in range(p_lo, p_hi):
++ self._head_gen[k] = gen
++ return True
+
+ def _ensure_text_block(self, j0: int, j1: int) -> None:
+ """Re-render one block, snapped out to whole ADDRESS groups.
+diff --git a/server/patch_server.py b/server/patch_server.py
+index f3cbe35..a34a4a9 100644
+--- a/server/patch_server.py
++++ b/server/patch_server.py
+@@ -576,6 +576,28 @@ def _idatui_spans(line):
+ return [[k, t] for k, t, _o in out], trimmed
+
+
++def _idatui_rows_digest(rows):
++ """A value that changes whenever any of ``rows`` would render differently.
++
++ Covers everything a client keeps off a row: address, kind, size, the plain
++ text, the symbol name and the colour spans (which is what makes it exact
++ rather than a heuristic -- two lines can collapse to the same text and still
++ be coloured differently).
++
++ Uses the interpreter's own ``hash``, deliberately. It never has to mean
++ anything outside this process: the client stores what a page hashed to when
++ it loaded it and hands the same number back to ask whether the page still
++ hashes to that. One worker, one process, one hash seed.
++ """
++ acc = 0
++ for r in rows:
++ sp = r.get("spans")
++ acc = hash((acc, r.get("ea"), r.get("kind"), r.get("size"),
++ r.get("text"), r.get("name"),
++ tuple(map(tuple, sp)) if sp else None))
++ return acc
++
++
+ def _idatui_unknown_row(ea, size):
+ """One collapsed row for a run of ``size`` undefined bytes starting at
+ ``ea``. A single byte is rendered normally (shows its value); a longer run
+@@ -661,6 +683,7 @@ def heads(
+ end: Annotated[str, "Optional exclusive end address; default = segment end"] = "",
+ back: Annotated[bool, "Walk backwards: return the count heads ENDING just before addr, in forward order"] = False,
+ annotate: Annotated[bool, "Emit IDA-style function boundary banner rows (kind sep/funchdr)"] = False,
++ digest: Annotated[bool, "Return only a digest+count of the rows, not the rows themselves"] = False,
+ ) -> dict:
+ """Walk item heads from ``addr`` as a flat listing: every head is rendered
+ (code OR data OR undefined) via generate_disasm_line and stepped with
+@@ -767,7 +790,17 @@ def heads(
+ rows.extend(_rows_for(ea)) # a struct head expands into member rows
+ ea = _advance(ea)
+ cursor = {"next": hex(ea)} if more else {"done": True}
+- return {"addr": str(addr), "heads": rows, "cursor": cursor}
++ out = {"addr": str(addr), "cursor": cursor,
++ "digest": _idatui_rows_digest(rows), "count": len(rows)}
++ # ``digest`` mode answers "is this page still exactly what you have?" without
++ # shipping it. The rows are built either way -- generate_disasm_line is the
++ # floor and there is no way to know a line is unchanged without rendering it
++ # -- but pickling several hundred rows with their colour spans, unpickling
++ # them and rebuilding Heads is about 40% of what a page costs, and after a
++ # rename almost every page comes back identical.
++ if not digest:
++ out["heads"] = rows
++ return out
+
+
+ @tool