aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-10 01:51:28 +0200
committerblasty <blasty@local>2026-08-10 01:51:28 +0200
commitaf1aa95b3a140988b101ee32a1ed9602f4645d56 (patch)
tree66ecdeca18f5831ae796f425c3961de167cb74d1
parentBuild the listing index in one call: boot 9.3s -> 1.25s, 911 calls -> 4 (diff)
downloadida-tui-af1aa95b3a140988b101ee32a1ed9602f4645d56.tar.gz
ida-tui-af1aa95b3a140988b101ee32a1ed9602f4645d56.tar.xz
ida-tui-af1aa95b3a140988b101ee32a1ed9602f4645d56.zip
Fix: rebuilding the row index on every switch back to the listingHEADmain
Reported as "decompiler -> listing feels slow", and it was: ~900ms per Tab. The listing view re-primes every time it is shown, and priming now builds the whole row index. build_from_index() had no idempotence, so each switch back re-ran segment_index over the entire segment. Measured on bash, going back to the listing away from the primed viewport: press Tab: 904ms -> 9.7ms, and 5 backend calls -> 0 The index is a pure function of the database and the model is thrown away and rebuilt whenever anything moves the walk (stale_structure), so a model that is already indexed can return immediately. Nothing caught this because the listing was CORRECT the whole time -- only slow. Every structural assertion passed, boot still measured fast, and the suite has no notion of "how many calls did that keystroke cost". The new scenario counts backend calls across three view switches and asserts segment_index is not among them; with the guard removed again it fails. Latent and NOT fixed here: materialising a page the viewport reaches for the first time still happens inside render_line, i.e. an RPC (~20ms) on the UI loop. That predates this change -- it is how skeleton pages have always worked -- and is small enough not to read as a stall, but it is the same shape of bug and wants prefetching onto the worker that already exists for pages. Full gate: 1064 passed.
-rw-r--r--idatui/domain.py7
-rw-r--r--tests/test_scenarios.py42
2 files changed, 49 insertions, 0 deletions
diff --git a/idatui/domain.py b/idatui/domain.py
index ecc6a80..9690a65 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -820,7 +820,14 @@ class ListingModel:
Returns False if the backend cannot supply it, in which case the caller
should stream as before -- this is an optimisation, not a new contract.
+
+ IDEMPOTENT, and that is load-bearing: the view re-primes on every switch
+ back to the listing, so rebuilding here unconditionally put a ~900ms
+ segment_index in front of every Tab out of the decompiler.
"""
+ with self._lock:
+ if self._done and self._heads:
+ return True # already indexed; re-priming is a no-op
try:
idx = self._prog.client.invoke(
"segment_index", addr=hex(self.seg_start), end=hex(self.seg_end),
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 0bbcf17..c08dc96 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -492,6 +492,48 @@ async def s_segment_index(c: Ctx):
== [(h.ea, h.kind, h.size) for h in streamed._heads])
+@scenario("reprime_is_free")
+async def s_reprime_is_free(c: Ctx):
+ """Switching back to the listing must not rebuild the row index.
+
+ The listing view re-primes every time it is shown, and priming builds the
+ whole index. Building it is ~600-900ms, so doing it again on each Tab out of
+ the decompiler put nearly a second in front of a keystroke -- the listing
+ was still CORRECT, which is why every other test passed, it was just slow.
+
+ Counting backend calls is the only way to see that, so this counts them.
+ """
+ app = c.app
+ await c.open_biggest("listing")
+ lv = app.query_one(ListingView)
+ if lv.model is None:
+ c.check("listing model exists", False)
+ return
+ await c.wait(lambda: lv.model.complete, 30)
+
+ client = app.program.client
+ original = type(client).invoke
+ seen: list[str] = []
+
+ def counting(self, operation, *a, **kw):
+ seen.append(operation)
+ return original(self, operation, *a, **kw)
+
+ type(client).invoke = counting
+ try:
+ for _ in range(3): # decomp and back, three times
+ await c.press("tab")
+ await c.pause(0.05)
+ await c.press("tab")
+ await c.pause(0.05)
+ finally:
+ type(client).invoke = original
+
+ rebuilds = seen.count("segment_index")
+ c.check("switching views never rebuilds the segment index", rebuilds == 0,
+ f"segment_index called {rebuilds}x during 3 view switches: {seen}")
+
+
@scenario("skeleton_pages")
async def s_skeleton_pages(c: Ctx):
"""The background grower loads text-less pages; reading one must fill it in.