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 /idatui/domain.py | |
| 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 'idatui/domain.py')
| -rw-r--r-- | idatui/domain.py | 134 |
1 files changed, 83 insertions, 51 deletions
diff --git a/idatui/domain.py b/idatui/domain.py index 1f8263d..f4aa63c 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -642,11 +642,6 @@ class ListingModel: """ PAGE = 500 # heads per server call (well under the tool's 2000 cap) - #: Heads refreshed together when a rename makes their text stale. One server - #: call per block, so a viewport costs one round trip rather than forty -- - #: and the same size as a load page, so refreshing everything costs about - #: what rebuilding everything would have. - TEXT_BLOCK = 500 def __init__(self, program: "Program", seg_start: int, seg_end: int, name: str | None = None): @@ -673,6 +668,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 + #: One entry per loaded PAGE: where its heads start, the address it was + #: fetched from, the digest it came back with, and how many rows it + #: held. A stale-text refresh re-asks for exactly that page, so it can + #: be told "still identical" for the price of the render alone. + 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 +761,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 @@ -919,69 +927,93 @@ class ListingModel: def _ensure_text(self, j0: int, j1: int) -> None: """Re-render physical heads [j0, j1) if a rename staled them. - Done a block at a time. One call for the whole range would be simpler but - the ``heads`` tool caps a response at 2000 rows, so a wide request (the - 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. + Works a PAGE at a time -- the same unit the loader fetched. A page is + exactly what ``heads(addr, count=PAGE)`` produced, so asking again with + the same arguments reproduces the same row sequence; nothing has to be + snapped out to whole address groups (a function start emits three banner + rows sharing one address, and an arbitrary boundary through those never + lines up again). It also means every head in a page can share one + generation marker, so "is this fresh?" is a single probe. """ - 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 + j1 = min(j1, n) + j0 = max(j0, 0) + if j1 <= j0: + return + p = max(bisect.bisect_right(self._page_head, j0) - 1, 0) + last = bisect.bisect_left(self._page_head, j1) + while p < last: + p = self._ensure_page(p) - def _ensure_text_block(self, j0: int, j1: int) -> None: - """Re-render one block, snapped out to whole ADDRESS groups. + def _page_bounds(self, p: int) -> tuple[int, int]: + """[first, last) head index of page ``p`` (caller holds the lock).""" + lo = self._page_head[p] + hi = (self._page_head[p + 1] if p + 1 < len(self._page_head) + else len(self._heads)) + return lo, hi - A function start emits three banner rows and its code row at the same ea, - and a labelled instruction emits two -- so a boundary falling inside one - of those groups would refetch the whole group, never line up, and leave - the old names on screen for good. - """ + def _ensure_page(self, p: int) -> int: + """Freshen page ``p``; returns the next page to consider.""" with self._lock: + if not (0 <= p < len(self._page_head)): + return p + 1 gen = self._text_gen - n = len(self._heads) - a = max(j0, 0) - b = min(j1, n) - if b <= a: - return - head_gen = self._head_gen - if all(head_gen[j] == gen for j in range(a, b)): - return - eas = self._head_eas - while a > 0 and eas[a - 1] == eas[a]: - a -= 1 - while b < n and eas[b - 1] == eas[b]: - b += 1 - last = self._heads[b - 1] - lo = eas[a] - hi = last.ea + max(last.size, 1) - want = [(h.ea, h.kind) for h in self._heads[a:b]] + lo, hi = self._page_bounds(p) + if hi <= lo or self._head_gen[lo] == gen: + return p + 1 + addr = self._page_addr[p] + want_digest = self._page_digest[p] + want_rows = self._page_rows[p] + want = [(h.ea, h.kind) for h in self._heads[lo:hi]] + # Ask whether the page still renders as the client holds it. The worker + # builds the rows either way (there is no knowing a line is unchanged + # without rendering it), but skipping the pickling, the transfer, the + # unpickling and the Head rebuild is about 40% of what a page costs -- + # and after a rename almost every page is unchanged. + if want_digest is not None: + try: + probe = 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 + probe = None + if (isinstance(probe, dict) and probe.get("digest") == want_digest + and probe.get("count") == want_rows): + with self._lock: + if self._text_gen == gen and len(self._heads) >= hi: + for k in range(lo, hi): + self._head_gen[k] = gen + return p + 1 try: payload = self._prog.client.call( - "heads", addr=hex(lo), end=hex(hi), - count=min(len(want) + 64, 2000), annotate=True) + "heads", addr=hex(addr), count=self.PAGE, annotate=True) except Exception: # noqa: BLE001 -- keep the old text rather than blank - return + return p + 1 rows = payload.get("heads", []) if isinstance(payload, dict) else [] - page = self._build_page(rows)[:len(want)] + page = self._build_page(rows) with self._lock: - if self._text_gen != gen or len(self._heads) < b: - return + if self._text_gen != gen or len(self._heads) < hi: + return p + 1 if [(h.ea, h.kind) for h in page] != want: # Something moved the walk, which a rename cannot do -- so this # was not one. Say so and let Program.listing() rebuild, rather - # than sit here re-fetching a block that will never line up (and + # than sit here re-fetching a page that will never line up (and # showing the old names while doing it). self.stale_structure = True - for j in range(a, b): - self._head_gen[j] = gen - return - self._heads[a:b] = page - for j in range(a, b): - self._head_gen[j] = gen + for k in range(lo, hi): + self._head_gen[k] = gen + return p + 1 + self._heads[lo:hi] = page + # The stored digest has to describe what the client now HOLDS, not + # what it once loaded. Leaving it stale is how a literal cycling + # hex -> dec -> hex ends up declared "unchanged" while the row still + # shows the decimal it was refetched with in between. + self._page_digest[p] = (payload.get("digest") + if isinstance(payload, dict) else None) + for k in range(lo, hi): + self._head_gen[k] = gen + return p + 1 def get(self, i: int) -> Head | None: with self._lock: |
