diff options
| author | user <user@clank> | 2026-08-07 03:28:55 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-07 03:28:55 +0200 |
| commit | 98b3b98eb1bf8f195ef4ebb1375c9f161d758c17 (patch) | |
| tree | 810ec6ef2e88453a9fc076216dbc8a9b7e797a45 /idatui | |
| parent | Baseline for the v5 bench (landing polls every 2ms instead of 10ms; the poll ... (diff) | |
| download | ida-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.py | 41 | ||||
| -rw-r--r-- | idatui/graph.py | 47 |
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 |
