aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/app.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-23 01:59:16 +0200
committerblasty <blasty@local>2026-07-23 01:59:16 +0200
commitbfdbb469e45ce66d82723da307efc49c17d5f074 (patch)
treeaac2bd32e63da2571c20ab68712be14000731b10 /idatui/app.py
parentapp: 'a' — make string literal in the listing (M4 richness) (diff)
downloadida-tui-bfdbb469e45ce66d82723da307efc49c17d5f074.tar.gz
ida-tui-bfdbb469e45ce66d82723da307efc49c17d5f074.tar.xz
ida-tui-bfdbb469e45ce66d82723da307efc49c17d5f074.zip
perf: stream the listing incrementally instead of load_all on open (M4)
Opening a listing walked the WHOLE segment (load_all) before showing anything -- a blank pane for seconds on a big .text. Now _prime loads just the viewport around the cursor (appears instantly) and _grow streams the rest in the background, growing virtual_size as pages land (throttled). Search finishes loading first (needs the whole segment). ListingModel gains a _load_lock so the grower and an in-view search can both drive page loads without double-fetching; new public load_next_page. Verified: ensure(1100)->partial(1500,incomplete), load_all->full(5036,complete); 3 concurrent loaders -> 0 duplicate heads; UI listing+search 30/0 (in-process).
Diffstat (limited to 'idatui/app.py')
-rw-r--r--idatui/app.py54
1 files changed, 49 insertions, 5 deletions
diff --git a/idatui/app.py b/idatui/app.py
index cc9ab03..27dc33e 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -961,10 +961,16 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
model = self.model
if model is None:
return
- model.load_all() # walk the whole segment's heads off the UI loop
- self.app.call_from_thread(self._on_primed, len(model))
+ # Load just enough to render the viewport around the cursor, so the
+ # listing appears immediately even on a huge segment; the rest streams
+ # in via _grow. (load_all here would blank the pane for seconds.)
+ height = max(self.size.height, 1)
+ model.ensure(self.cursor + height + 2 * ListingModel.PAGE)
+ self.app.call_from_thread(self._on_primed, len(model), model.complete)
+ if not model.complete:
+ self._grow()
- def _on_primed(self, total: int) -> None:
+ def _on_primed(self, total: int, complete: bool) -> None:
if self.model is None:
return
self.total = total
@@ -979,7 +985,33 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
self.refresh()
self.post_message(ListingView.CursorMoved(self.cursor, self._cursor_ea()))
- # -- search hooks (all heads are in memory after prime) --------------- #
+ @work(thread=True, exclusive=True, group="listing-grow")
+ def _grow(self) -> None:
+ """Stream the rest of the segment's heads in the background, growing the
+ virtual size as they land so the scrollbar/paging catch up."""
+ model = self.model
+ if model is None:
+ return
+ since = 0
+ while not model.complete:
+ if model.load_next_page() == 0:
+ break
+ if self.model is not model: # a new load() replaced us
+ return
+ since += 1
+ if since >= 4: # throttle repaints on huge segments
+ since = 0
+ self.app.call_from_thread(self._grew, len(model))
+ self.app.call_from_thread(self._grew, len(model))
+
+ def _grew(self, total: int) -> None:
+ if self.model is None or total <= self.total:
+ return
+ self.total = total
+ self.virtual_size = Size(0, total)
+ self.refresh()
+
+ # -- search hooks ----------------------------------------------------- #
def _search_line_count(self) -> int:
return self.total
@@ -987,7 +1019,19 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
return self._line_plain(i)
def _search_ensure(self, done) -> None:
- done()
+ # Search needs the whole segment; finish loading it first if streaming.
+ if self.model is not None and not self.model.complete:
+ self._search_load_all(done)
+ else:
+ done()
+
+ @work(thread=True, exclusive=True, group="listing-search-load")
+ def _search_load_all(self, done) -> None: # type: ignore[no-untyped-def]
+ model = self.model
+ if model is not None:
+ model.load_all()
+ self.app.call_from_thread(self._grew, len(model))
+ self.app.call_from_thread(done)
# -- rendering --------------------------------------------------------- #
def render_line(self, y: int) -> Strip: