summaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-07 08:01:25 +0200
committeruser <user@clank>2026-08-07 08:01:25 +0200
commit16318e4bfd939666bc45fa6f5d2c17cb8f374f3b (patch)
treee0395a59d86bb7a20899aa06b7569cf781fb1e93 /idatui
parentThree redundancies in the heads walk: item flags were fetched three times per... (diff)
downloadida-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.py74
-rw-r--r--idatui/edit_ctl.py10
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()