diff options
| author | blasty <peter@haxx.in> | 2026-08-07 02:00:06 +0200 |
|---|---|---|
| committer | blasty <peter@haxx.in> | 2026-08-07 02:00:06 +0200 |
| commit | f9bb8491d612bb6018c7b820a4b1c84790a53cd1 (patch) | |
| tree | 22b4eb4c55c5802dcc2efce3fc6f6eef52351591 /idatui | |
| parent | Re-apply #5 (lru_cache on the per-line render + Heads built with their opcode... (diff) | |
| download | ida-tui-f9bb8491d612bb6018c7b820a4b1c84790a53cd1.tar.gz ida-tui-f9bb8491d612bb6018c7b820a4b1c84790a53cd1.tar.xz ida-tui-f9bb8491d612bb6018c7b820a4b1c84790a53cd1.zip | |
Incremental search narrows instead of rescanning. Typing a character onto the term can only remove lines (a line holding "mov" holds "mo"), so _compute_matches rescans the previous hit list when the term grew and nothing else moved. Keyed on (term, case-fold, row count, line-source id) so a listing still streaming rows in behind the search falls back to a full scan.
Result: {"status":"keep","total_ms":20835.7,"lg_boot_ms":754.4,"lg_decomp_ms":2593.7,"lg_graph_ms":929.5,"lg_hex_ms":1080.2,"lg_index_ms":76.1,"lg_listing_cold_ms":526.7,"lg_listing_warm_ms":410.1,"lg_nav_ms":6674.8,"lg_palette_ms":5,"lg_render_ms":225.2,"lg_search_ms":3310.3,"pure_graph_ms":239.3,"sm_boot_ms":537.9,"sm_decomp_ms":630.1,"sm_graph_ms":734.3,"sm_hex_ms":869.9,"sm_index_ms":0,"sm_listing_cold_ms":260.8,"sm_listing_warm_ms":262.3,"sm_nav_ms":334.8,"sm_palette_ms":0.3,"sm_render_ms":268.4,"sm_search_ms":111.6,"fails":0}
Diffstat (limited to 'idatui')
| -rw-r--r-- | idatui/app.py | 65 |
1 files changed, 51 insertions, 14 deletions
diff --git a/idatui/app.py b/idatui/app.py index 61f366e..0af47c0 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -629,6 +629,17 @@ class SearchMixin: _matches: list[int] _ranges: dict[int, list[tuple[int, int]]] + #: What ``_matches`` was last computed from: (term, case-fold, row count, + #: id(line source)). Lets an as-you-type search narrow the previous hits + #: instead of rescanning the segment; see :meth:`_compute_matches`. Any of + #: those changing under us discards it. + _matched_key: tuple | None = None + + def _search_source_id(self) -> int: + """Identity of whatever supplies the line text. Changes when the view is + pointed at a different model/body, which invalidates a narrowing.""" + return id(getattr(self, "model", None) or getattr(self, "_texts", None)) + # --- hooks a subclass implements --- def _search_line_count(self) -> int: raise NotImplementedError @@ -660,6 +671,7 @@ class SearchMixin: self._term = "" self._matches = [] self._ranges = {} + self._matched_key = None self.cursor = getattr(self, "_search_origin", self.cursor) self.cursor_x = getattr(self, "_search_origin_x", self.cursor_x) self.refresh() @@ -698,6 +710,7 @@ class SearchMixin: self._term = "" self._matches = [] self._ranges = {} + self._matched_key = None self.cursor = getattr(self, "_search_origin", self.cursor) self.cursor_x = getattr(self, "_search_origin_x", self.cursor_x) self.scroll_to(y=max(self.cursor - self._visible_height() // 2, 0), animate=False) @@ -715,26 +728,46 @@ class SearchMixin: def _compute_matches(self) -> None: term = self._term - needle = term.lower() if getattr(self, "_ci", True) else term + ci = getattr(self, "_ci", True) + needle = term.lower() if ci else term + count = self._search_line_count() + src = self._search_source_id() + # Typing forward can only ever REMOVE lines: a line holding "mov" holds + # "mo". So when the term just grew (and nothing else moved -- same + # case-folding, same body, same number of rows) rescan only the previous + # hits. Search is driven a keystroke at a time, and a segment of bash is + # 224k rows; this is the difference between rescanning all of them per + # keypress and looking at a few thousand. + # + # The row count is part of the key because the listing streams in behind + # the search: rows that arrived after the last pass have never been + # looked at, and narrowing would silently never find them. + rows: object = range(count) + prev = self._matched_key + if (prev is not None and prev[2] == count and prev[3] == src + and prev[1] == ci and term.startswith(prev[0]) and prev[0]): + rows = self._matches matches: list[int] = [] ranges: dict[int, list[tuple[int, int]]] = {} - for i in range(self._search_line_count()): - s = self._search_line_text(i) + text_of = self._search_line_text + n = len(term) + for i in rows: + s = text_of(i) if not s: continue - hay = s.lower() if getattr(self, "_ci", True) else s - pos, rs = 0, [] - while True: - j = hay.find(needle, pos) - if j < 0: - break - rs.append((j, j + len(term))) - pos = j + len(term) - if rs: - matches.append(i) - ranges[i] = rs + hay = s.lower() if ci else s + j = hay.find(needle) + if j < 0: + continue + rs = [] + while j >= 0: + rs.append((j, j + n)) + j = hay.find(needle, j + n) + matches.append(i) + ranges[i] = rs self._matches = matches self._ranges = ranges + self._matched_key = (term, ci, count, src) def search_repeat(self, direction: int, include_current: bool = False) -> None: if not getattr(self, "_term", ""): @@ -768,6 +801,7 @@ class SearchMixin: self._term = "" self._matches = [] self._ranges = {} + self._matched_key = None self.refresh() def _match_style(self, idx: int) -> Style: @@ -949,6 +983,7 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru self._pending_op = None self._matches = [] self._ranges = {} + self._matched_key = None self._prime() @work(thread=True, exclusive=True, group="listing-prime") @@ -1023,6 +1058,7 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru self._op_mode = (self._op_mode + 1) % 3 self._update_op_w() self._ranges = {} # column layout changed -> stale match offsets + self._matched_key = None # ...and which rows match at all self._clamp_x() self.refresh() self._app_status("opcodes: " + {0: "off", 1: f"limited ({_OP_LIMIT} bytes)", @@ -1458,6 +1494,7 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True self.cursor_x = cursor_x self._matches = [] self._ranges = {} + self._matched_key = None # Gutter wide enough for the largest line number + a trailing space. self._gutter = (len(str(total)) + 1) if total else 0 maxw = max((s.cell_length for s in self._strips), default=0) |
