summaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-07 03:28:55 +0200
committeruser <user@clank>2026-08-07 03:28:55 +0200
commit98b3b98eb1bf8f195ef4ebb1375c9f161d758c17 (patch)
tree810ec6ef2e88453a9fc076216dbc8a9b7e797a45 /idatui
parentBaseline for the v5 bench (landing polls every 2ms instead of 10ms; the poll ... (diff)
downloadida-tui-98b3b98eb1bf8f195ef4ebb1375c9f161d758c17.tar.gz
ida-tui-98b3b98eb1bf8f195ef4ebb1375c9f161d758c17.tar.xz
ida-tui-98b3b98eb1bf8f195ef4ebb1375c9f161d758c17.zip
Three targeted cuts: the graph's transposition pass counts keep and swap in one pass over the neighbour pairs (was four _pair_cross calls); the barycentre median answers degree 1 and 2 without sorting; and the search body is built from windowed model reads instead of one locked row lookup per line.
Result: {"status":"keep","total_ms":17944.4,"lg_boot_ms":703.7,"lg_decomp_ms":2453.7,"lg_graph_ms":1000,"lg_hex_ms":478,"lg_index_ms":96,"lg_listing_cold_ms":453,"lg_listing_warm_ms":411.2,"lg_nav_ms":6555.9,"lg_palette_ms":4.9,"lg_render_ms":218.4,"lg_search_ms":1308.8,"pure_graph_ms":212.3,"sm_boot_ms":432.4,"sm_decomp_ms":1267.8,"sm_graph_ms":742.7,"sm_hex_ms":440,"sm_index_ms":2.5,"sm_listing_cold_ms":264.3,"sm_listing_warm_ms":289.5,"sm_nav_ms":305.4,"sm_palette_ms":0.3,"sm_render_ms":258.6,"sm_search_ms":45,"fails":0}
Diffstat (limited to 'idatui')
-rw-r--r--idatui/app.py41
-rw-r--r--idatui/graph.py47
2 files changed, 75 insertions, 13 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 6c0a872..c37846f 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -690,15 +690,21 @@ class SearchMixin:
key = (count, src)
if self._hay_key == key:
return self._hay
- text_of = self._search_line_text
starts: list[int] = []
parts: list[str] = []
pos = 0
- for i in range(count):
- s = text_of(i) or ""
+ chunk = 4096
+ for base in range(0, count, chunk):
+ for s in self._search_line_texts(base, min(chunk, count - base)):
+ if not s:
+ s = ""
+ starts.append(pos)
+ parts.append(s)
+ pos += len(s) + 1
+ while len(starts) < count: # a short window: keep the indices lined up
starts.append(pos)
- parts.append(s)
- pos += len(s) + 1
+ parts.append("")
+ pos += 1
blob = "\n".join(parts)
folded = blob.lower()
hay = None if len(folded) != len(blob) else (starts, blob, folded)
@@ -713,6 +719,12 @@ class SearchMixin:
def _search_line_text(self, i: int) -> str | None:
raise NotImplementedError
+ def _search_line_texts(self, start: int, count: int) -> list:
+ """``count`` line texts from ``start``. Overridable so a view whose rows
+ come from a locked model can fetch a window in one go."""
+ text_of = self._search_line_text
+ return [text_of(i) for i in range(start, start + count)]
+
def _search_ensure(self, done) -> None:
"""Ensure all line texts are available, then call ``done()`` on the UI
thread. Default: assume ready."""
@@ -1031,8 +1043,7 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
return ""
return self._op_bytes_text(h).ljust(self._op_w) + " "
- def _line_plain(self, idx: int) -> str | None:
- h = self._head(idx)
+ def _plain_of(self, h: Head | None) -> str | None:
if h is None:
return None
# Function headers and code labels sit at depth 0 (with the address);
@@ -1045,6 +1056,22 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
extra = _LST_INDENT if h.kind == "member" else ""
return base + self._op_field(h) + extra + self._name_prefix(h) + h.text
+ def _line_plain(self, idx: int) -> str | None:
+ return self._plain_of(self._head(idx))
+
+ def _search_line_texts(self, start: int, count: int) -> list:
+ """A window of plain lines in one model call.
+
+ Building the search body row by row took the model's lock and bisected
+ its row table a quarter of a million times; ``window`` does both once
+ for the whole window.
+ """
+ model = self.model
+ if model is None:
+ return []
+ plain = self._plain_of
+ return [plain(h) for h in model.window(start, count)]
+
def _insn_col(self, idx: int) -> int:
"""Column where the instruction/content text begins, past the address +
opcode-bytes gutter — the shift+home target. Mirrors ``_line_plain``'s
diff --git a/idatui/graph.py b/idatui/graph.py
index e0a9422..baee597 100644
--- a/idatui/graph.py
+++ b/idatui/graph.py
@@ -280,6 +280,33 @@ def _pair_cross(a: int, b: int, side: dict[int, list[int]],
return n
+def _swap_delta(a: int, b: int, down: dict[int, list[int]],
+ up: dict[int, list[int]], pos: dict[int, int]) -> tuple[int, int]:
+ """``(keep, swap)`` for the adjacent pair (a, b), both sides, in one pass.
+
+ The same as calling :func:`_pair_cross` four times, which is what the
+ transposition loop used to do: every neighbour pair was visited twice (once
+ per direction) and each visit was a python call. Counting both outcomes
+ while the pair is in hand halves the comparisons and removes three calls per
+ candidate swap — and this runs a third of a million times over a corpus.
+ """
+ keep = swap = 0
+ for side in (down, up):
+ va = side[a]
+ vb = side[b]
+ if not va or not vb:
+ continue
+ pbs = [pos[v] for v in vb]
+ for u in va:
+ pu = pos[u]
+ for pv in pbs:
+ if pu > pv:
+ keep += 1
+ elif pu < pv:
+ swap += 1
+ return keep, swap
+
+
def crossings(layers: list[list[int]], down: dict[int, list[int]],
pos: dict[int, int]) -> int:
return sum(_cross_below(l, down, pos) for l in layers)
@@ -306,11 +333,20 @@ def _order_layers(g: _Graph, root: int, sweeps: int = 6) -> list[list[int]]:
pos = {i: k for layer in layers for k, i in enumerate(layer)}
def median(i: int, side: dict[int, list[int]]) -> float:
- ps = sorted(pos[j] for j in side[i])
- if not ps:
+ # Almost every node in a control-flow graph has one or two neighbours
+ # on a given side, so answer those without building and sorting a list:
+ # this runs tens of thousands of times per corpus layout.
+ js = side[i]
+ n = len(js)
+ if n == 1:
+ return float(pos[js[0]])
+ if n == 2:
+ return (pos[js[0]] + pos[js[1]]) / 2
+ if not n:
return -1.0
- m = len(ps) // 2
- return float(ps[m]) if len(ps) % 2 else (ps[m - 1] + ps[m]) / 2
+ ps = sorted(pos[j] for j in js)
+ m = n // 2
+ return float(ps[m]) if n % 2 else (ps[m - 1] + ps[m]) / 2
best, best_x = [list(l) for l in layers], crossings(layers, down, pos)
for s in range(sweeps):
@@ -327,8 +363,7 @@ def _order_layers(g: _Graph, root: int, sweeps: int = 6) -> list[list[int]]:
for layer in layers:
for k in range(len(layer) - 1):
a, b = layer[k], layer[k + 1]
- keep = _pair_cross(a, b, down, pos) + _pair_cross(a, b, up, pos)
- swap = _pair_cross(b, a, down, pos) + _pair_cross(b, a, up, pos)
+ keep, swap = _swap_delta(a, b, down, up, pos)
if swap < keep:
layer[k], layer[k + 1] = b, a
pos[a], pos[b] = k + 1, k