diff options
| author | user <user@clank> | 2026-08-07 08:01:25 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-07 08:01:25 +0200 |
| commit | 16318e4bfd939666bc45fa6f5d2c17cb8f374f3b (patch) | |
| tree | e0395a59d86bb7a20899aa06b7569cf781fb1e93 /idatui | |
| parent | Three redundancies in the heads walk: item flags were fetched three times per... (diff) | |
| download | ida-tui-16318e4bfd939666bc45fa6f5d2c17cb8f374f3b.tar.gz ida-tui-16318e4bfd939666bc45fa6f5d2c17cb8f374f3b.tar.xz ida-tui-16318e4bfd939666bc45fa6f5d2c17cb8f374f3b.zip | |
An item edit (c/d/u/p) keeps the listing's walk in front of it instead of discarding the model. bump_items now takes the edited address; rows before an edit keep their addresses and their row numbers, so only the pages from the edit onward are re-walked. Getting the listing back after undefining at the cursor on bash: 4890ms -> 19ms (257x). Adds .auto/check_edit.py to the gate. total_ms is flat — the bench has no item-edit phase, and the one I wrote hangs (reverted, cause recorded).
Result: {"status":"keep","total_ms":25783,"lg_boot_ms":777.9,"lg_decomp_ms":2381.9,"lg_graph_ms":1209.5,"lg_hex_ms":450,"lg_index_ms":68,"lg_listing_cold_ms":434.6,"lg_listing_warm_ms":405.2,"lg_nav_ms":6801.2,"lg_palette_ms":4.7,"lg_rename_ms":744.7,"lg_render_ms":222.4,"lg_search_ms":3885.6,"lg_split_ms":2261.4,"pure_graph_ms":216.4,"sm_boot_ms":463.6,"sm_decomp_ms":1304.6,"sm_graph_ms":740.9,"sm_hex_ms":438.4,"sm_index_ms":2.4,"sm_listing_cold_ms":270.3,"sm_listing_warm_ms":267.2,"sm_nav_ms":312.4,"sm_palette_ms":0.3,"sm_rename_ms":415.8,"sm_render_ms":257.7,"sm_search_ms":60.8,"sm_split_ms":1385.2,"fails":0}
Diffstat (limited to 'idatui')
| -rw-r--r-- | idatui/domain.py | 74 | ||||
| -rw-r--r-- | idatui/edit_ctl.py | 10 |
2 files changed, 75 insertions, 9 deletions
diff --git a/idatui/domain.py b/idatui/domain.py index f4aa63c..2cfd9d9 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -906,6 +906,51 @@ class ListingModel: def __len__(self) -> int: return self.loaded() + def truncate_from(self, ea: int) -> bool: + """Drop the walk from the page an edit at ``ea`` could have moved. + + An item edit changes structure, but only *locally*: every head before it + keeps its address and its row number. Throwing the whole model away made + the reload re-walk the segment -- 4.9 seconds on bash to make one byte + into data, for an edit the user made at the row they were looking at. + + Two pages are dropped rather than one, because undefining can coalesce + backwards into the run in front of it. Beyond that the caller marks the + kept prefix text-stale, so every kept page is digest-checked on the next + read and a page that really did move fails its sequence check and forces + a rebuild. Safe by construction, not by argument. + + Returns False if nothing worth keeping is left. + """ + with self._lock: + if not (self.seg_start <= ea < self.seg_end): + return True # another segment; nothing moved here + if len(self._page_head) < 3: + return False # barely walked; a rebuild is cheaper + p = bisect.bisect_right(self._page_addr, ea) - 1 + p = max(p - 1, 0) + if p <= 0: + return False # the edit is in the first pages + keep = self._page_head[p] + if keep <= 0: + return False + for h in self._heads[keep:]: + self._by_ea.pop(h.ea, None) + del self._heads[keep:] + del self._head_eas[keep:] + del self._head_gen[keep:] + del self._row_at[keep:] + del self._page_head[p:] + self._next = self._page_addr[p] + del self._page_addr[p:] + del self._page_digest[p:] + del self._page_rows[p:] + last = self._heads[-1] + self._rows = self._row_at[-1] + self._span(last) + self._done = False + self._ubytes.clear() # undefined-run bytes behind the drop point + return True + def invalidate_text(self) -> None: """A rename changed how rows READ, not which rows exist. @@ -1589,22 +1634,41 @@ class Program: for lm in listings: lm.invalidate_text() - def bump_items(self) -> None: + def bump_items(self, ea: int | None = None) -> None: """Signal that item/function STRUCTURE changed (define code/data/func, undefine). Unlike a rename this can move instruction boundaries and - change function membership anywhere, so drop the disasm block caches, - the decompilation cache and the cached function indices outright, and - bump the name generation too (labels/names may appear or vanish).""" + change function membership, so drop the disasm block caches, the + decompilation cache and the cached function indices outright, and bump + the name generation too (labels/names may appear or vanish). + + Given the address that was edited, the segment listing keeps the walk in + front of it instead of being thrown away: the rows before an edit keep + their addresses and their row numbers. Without ``ea`` this falls back to + discarding the listings, as it always did. + """ with self._lock: self._name_gen += 1 self._indices.clear() self._decomp.clear() - self._listings.clear() self._pc_nums.clear() models = list(self._disasm.values()) self._disasm.clear() + listings = list(self._listings.items()) + if ea is None: + self._listings.clear() for m in models: m.invalidate() + if ea is None: + return + for start, lm in listings: + if lm.truncate_from(ea): + # Names can move too; the kept prefix is re-rendered on demand, + # and that is also what catches a page the edit really did move. + lm.invalidate_text() + else: + with self._lock: + if self._listings.get(start) is lm: + del self._listings[start] # -- item / function structure edits (IDA c/d/u/p) --------------------- # @staticmethod diff --git a/idatui/edit_ctl.py b/idatui/edit_ctl.py index 54f4d89..435fab9 100644 --- a/idatui/edit_ctl.py +++ b/idatui/edit_ctl.py @@ -288,7 +288,7 @@ class EditController: app.call_from_thread(app._status, err) return # The label shows in the listing's head rows -> invalidate + reopen. - app.program.bump_items() + app.program.bump_items(addr) # Naming the address *of a function start* is a function rename by any # other name. Without this the cached index kept the old name, so # `functions`/`names`/resolve/the palette all reported the rename had @@ -510,7 +510,7 @@ class EditController: diag.note(f"make_data({ea:#x}, {type_decl!r})", e) app.call_from_thread(app._status, f"make data: {e}") return - app.program.bump_items() + app.program.bump_items(ea) anchor = anchor or _M().ViewAnchor() anchor.flash = f"data ({type_decl}) @ {ea:#x} (Ctrl+S to save)" name = app.program.region_label(ea) @@ -707,8 +707,10 @@ class EditController: diag.note(f"edit_item({kind}, {ea:#x})", e) app.call_from_thread(app._status, f"{kind}: {e}") return - # Structure changed everywhere: drop all item/function/decomp caches. - app.program.bump_items() + # Structure changed: drop all item/function/decomp caches. The segment + # listing keeps its walk in front of `ea` -- rows before an edit keep + # their addresses and their row numbers. + app.program.bump_items(ea) # Re-resolve: a define_func upgrades the region to a real function view; # anything else re-reads the (still function-less) listing in place. anchor = anchor or _M().ViewAnchor() |
