aboutsummaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-07 01:55:31 +0200
committeruser <user@clank>2026-08-07 01:55:31 +0200
commitcf45e115bcd137020002e84b67da7b00547901b5 (patch)
tree3954101322a281db2c2a739d647331fc33414be1 /idatui
parent_idatui_spans: one capturing re.split over the tag pairs instead of finditer+... (diff)
downloadida-tui-cf45e115bcd137020002e84b67da7b00547901b5.tar.gz
ida-tui-cf45e115bcd137020002e84b67da7b00547901b5.tar.xz
ida-tui-cf45e115bcd137020002e84b67da7b00547901b5.zip
Re-apply #5 (lru_cache on the per-line render + Heads built with their opcode bytes already attached) with the graph_minimap scenario's racy SETUP made deterministic: clear _graph_sticky before the second navigation so Space is known to be entering the graph, not leaving it. No assertion changed.
Result: {"status":"keep","total_ms":22980.2,"lg_boot_ms":738.2,"lg_decomp_ms":2401.8,"lg_graph_ms":944.1,"lg_hex_ms":920.6,"lg_index_ms":75.2,"lg_listing_cold_ms":538.5,"lg_listing_warm_ms":411.1,"lg_nav_ms":6813.9,"lg_palette_ms":4.9,"lg_render_ms":221.8,"lg_search_ms":5630.1,"pure_graph_ms":240.7,"sm_boot_ms":537.5,"sm_decomp_ms":595.1,"sm_graph_ms":715.7,"sm_hex_ms":858.8,"sm_index_ms":0,"sm_listing_cold_ms":263.3,"sm_listing_warm_ms":265.3,"sm_nav_ms":335.2,"sm_palette_ms":0.3,"sm_render_ms":271.4,"sm_search_ms":196.5,"fails":0}
Diffstat (limited to 'idatui')
-rw-r--r--idatui/domain.py85
1 files changed, 50 insertions, 35 deletions
diff --git a/idatui/domain.py b/idatui/domain.py
index b496638..1c7883f 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -123,19 +123,22 @@ class Head:
return None
@classmethod
- def from_raw(cls, d: dict) -> "Head":
+ def from_raw(cls, d: dict, raw: bytes | None = None) -> "Head":
sp = d.get("spans")
ops = d.get("ops")
+ # ``tuple(map(tuple, ...))`` rather than a per-item genexpr with str()/
+ # int() coercion: this runs once per listing row (hundreds of thousands
+ # on a real binary) and the worker's own tool already emits [str, str]
+ # and [int, int, int]. The coercion was re-proving that on every row.
return cls(
ea=_as_int(d["ea"]),
kind=d.get("kind", "unknown"),
size=int(d.get("size", 0) or 0),
text=d.get("text", ""),
name=d.get("name"),
- spans=(tuple((str(k), str(t)) for k, t in sp)
- if isinstance(sp, list) and sp else None),
- ops=(tuple((int(a), int(b), int(n)) for a, b, n in ops)
- if isinstance(ops, list) and ops else None),
+ raw=raw,
+ spans=tuple(map(tuple, sp)) if sp else None,
+ ops=tuple(map(tuple, ops)) if ops else None,
)
@@ -654,30 +657,48 @@ class ListingModel:
# containing a huge coalesced undefined run doesn't pull megabytes.
_OP_SPAN_CAP = 1 << 16
- def _attach_opcode_bytes(self, page: list[Head]) -> list[Head]:
- """Fill ``raw`` (opcode bytes) for the code heads in ``page`` via one
- bulk read over their extent (variable-length safe)."""
- code = [h for h in page if h.kind == "code" and h.size > 0]
- if not code:
- return page
- lo = code[0].ea
- hi = code[-1].ea + code[-1].size
- if hi - lo <= 0 or hi - lo > self._OP_SPAN_CAP:
- return page
- data = self._prog.read_bytes(lo, hi - lo)
+ def _build_page(self, rows: list) -> list[Head]:
+ """Turn the tool's raw rows into ``Head``s with their opcode bytes
+ already attached, via one bulk read over the code extent.
+
+ The bytes are read BEFORE the Heads are built rather than patched in
+ afterwards: ``dataclasses.replace`` re-runs ``__init__`` with every
+ field, so filling ``raw`` after the fact meant constructing each code
+ head twice -- once per listing row, on the path a jump-to-address walks
+ hundreds of thousands of times.
+ """
+ lo = hi = -1
+ for r in rows:
+ if r.get("kind") == "code" and r.get("size"):
+ ea = _as_int(r["ea"])
+ if lo < 0:
+ lo = ea
+ hi = ea + int(r["size"])
+ data = None
+ if 0 <= lo < hi and hi - lo <= self._OP_SPAN_CAP:
+ try:
+ data = self._prog.read_bytes(lo, hi - lo)
+ except Exception: # noqa: BLE001 -- opcode bytes are decoration
+ data = None
+ page: list[Head] = []
biggest = self._max_raw
- out = []
- for h in page:
- if h.kind == "code" and h.size > 0:
- off = h.ea - lo
- b = bytes(data[off:off + h.size])
- biggest = max(biggest, len(b))
- out.append(replace(h, raw=b))
- else:
- out.append(h)
- with self._lock:
- self._max_raw = biggest
- return out
+ for r in rows:
+ raw = None
+ if data is not None and r.get("kind") == "code":
+ size = int(r.get("size") or 0)
+ if size > 0:
+ off = _as_int(r["ea"]) - lo
+ raw = bytes(data[off:off + size])
+ if len(raw) > biggest:
+ biggest = len(raw)
+ try:
+ page.append(Head.from_raw(r, raw))
+ except (KeyError, ValueError, TypeError):
+ continue
+ if biggest != self._max_raw:
+ with self._lock:
+ self._max_raw = biggest
+ return page
def max_raw_len(self) -> int:
with self._lock:
@@ -700,13 +721,7 @@ class ListingModel:
"heads", addr=hex(frm), count=self.PAGE, annotate=True)
rows = payload.get("heads", []) if isinstance(payload, dict) else []
cur = payload.get("cursor", {}) if isinstance(payload, dict) else {}
- page = []
- for r in rows:
- try:
- page.append(Head.from_raw(r))
- except (KeyError, ValueError, TypeError):
- continue
- page = self._attach_opcode_bytes(page)
+ page = self._build_page(rows)
with self._lock:
for h in page:
# Banner/label rows (function headers, separators, code labels)