diff options
| author | user <user@clank> | 2026-08-07 06:45:52 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-07 06:45:52 +0200 |
| commit | d9e8fdb79136dfa32e01631aed8917c1cb9c1b54 (patch) | |
| tree | 53c4e6c440f8fa5ee19f2e8ae5b3a749ab28583b /.auto/wip-digest.patch | |
| parent | Size the worker's per-line render cache to hold a segment's DISTINCT lines (1... (diff) | |
| download | ida-tui-d9e8fdb79136dfa32e01631aed8917c1cb9c1b54.tar.gz ida-tui-d9e8fdb79136dfa32e01631aed8917c1cb9c1b54.tar.xz ida-tui-d9e8fdb79136dfa32e01631aed8917c1cb9c1b54.zip | |
heads(digest=True): the worker answers "does this page still render exactly as you hold it?" with a hash and a count instead of the page. After a rename nearly every page is unchanged, so the pickling, transfer, unpickling and Head rebuild are all skipped. Redone at PAGE granularity end to end, which fixes both bugs of the first attempt. lg_search 5628 -> 3959.
Result: {"status":"keep","total_ms":26491.7,"lg_boot_ms":808.6,"lg_decomp_ms":2600.2,"lg_graph_ms":901.2,"lg_hex_ms":448.7,"lg_index_ms":70.3,"lg_listing_cold_ms":432.8,"lg_listing_warm_ms":445.5,"lg_nav_ms":7004.8,"lg_palette_ms":4.8,"lg_rename_ms":758.6,"lg_render_ms":231.3,"lg_search_ms":3959.2,"lg_split_ms":2659.2,"pure_graph_ms":214.7,"sm_boot_ms":432.6,"sm_decomp_ms":1305.1,"sm_graph_ms":758.3,"sm_hex_ms":431.8,"sm_index_ms":2.3,"sm_listing_cold_ms":275.4,"sm_listing_warm_ms":289.4,"sm_nav_ms":295.6,"sm_palette_ms":0.3,"sm_rename_ms":424.3,"sm_render_ms":263.6,"sm_search_ms":61.1,"sm_split_ms":1411.9,"fails":0}
Diffstat (limited to '.auto/wip-digest.patch')
| -rw-r--r-- | .auto/wip-digest.patch | 162 |
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 |
