From 23ee522a8b4fed9de62cd3bdfac8656c606a9d3c Mon Sep 17 00:00:00 2001 From: blasty Date: Sat, 25 Jul 2026 14:41:17 +0200 Subject: palette: cut the per-keystroke cost of project-wide symbol search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported as sluggish up/down navigation in project scope. Measuring it turned up two things: * The arrow keys themselves aren't the problem, and it isn't list length: a synthetic down-key benchmark reads a flat ~62 ms/key from 20 to 400 options in BOTH scopes. That is a fixed floor in pilot.press() (Textual awaits a full refresh cycle per synthetic press), so the harness can't isolate real key latency — the number says nothing about the app. * What IS measurably worse in project scope is the work done per typed character: 29.5 ms vs 17.8 ms locally. Fix the part that's real. The trigram index already guarantees every hit contains the query, so ranking only has to ORDER them — an exact-substring rank (match position, then name length) costs one find() per row instead of a full fuzzy pass, and fetching 3x the display limit instead of 10x cuts the candidate set. As a bonus the highlight spans come straight from the match position. Per-keystroke _apply over a 30k-entry, 6-binary index, typing "m"->"main_": before project ~29.5 ms after project 10.8 / 11.2 / 11.9 / 13.2 / 14.2 ms (local, unchanged, for reference: 13.8 / 14.3 / 15.2 / 16.2 / 38.8 ms) Project scope is now cheaper per keystroke than local scope, whose fuzzy pass over every function is the remaining hot spot. Suite 191/0. --- idatui/app.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/idatui/app.py b/idatui/app.py index bbb34a7..f589cea 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -2108,17 +2108,22 @@ class SymbolPalette(ModalScreen): # rows: (binary|None, addr, name, match positions) if self._project_scope and self._index is not None: from .index import KIND_FUNC - # Two stages: the trigram index narrows across every binary (cheap, - # and works for binaries with no live worker), then the same fuzzy - # ranking as local scope orders what's left. + # The trigram index already guarantees every hit CONTAINS the query, + # so ranking only has to order them — an exact-substring rank (match + # position, then name length) costs a find() per row instead of a + # full fuzzy pass, and fetching 3x the display limit rather than 10x + # keeps the per-keystroke work down on a big project. hits = self._index.search(query, kind=KIND_FUNC, - limit=self.LIMIT * 10) if query else [] + limit=self.LIMIT * 3) if query else [] + q = query.lower() scored = [] for h in hits: - m = _fuzzy(h.text, query) - scored.append(((m[0] if m else 0.0), (m[1] if m else ()), h)) - scored.sort(key=lambda t: (-t[0], t[2].text)) - rows = [(h.binary, h.addr, h.text, pos) for _, pos, h in scored[:self.LIMIT]] + at = h.text.lower().find(q) + scored.append((at if at >= 0 else 1 << 30, len(h.text), h.text, h)) + scored.sort() + rows = [(h.binary, h.addr, h.text, + tuple(range(at, at + len(q))) if at < (1 << 30) else ()) + for at, _, _, h in scored[:self.LIMIT]] elif query: scored = [] for f in self._funcs: -- cgit v1.3.1-sl0p