diff options
| author | blasty <blasty@local> | 2026-08-10 01:42:14 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-08-10 01:42:14 +0200 |
| commit | 82d5151c58ea6ed5681525707e403f6fa4160aa7 (patch) | |
| tree | 8c36f864689560e4e3e4733e2657ba23bb99f8a6 /tests | |
| parent | segment_index: the row total in one call instead of 458 (diff) | |
| download | ida-tui-82d5151c58ea6ed5681525707e403f6fa4160aa7.tar.gz ida-tui-82d5151c58ea6ed5681525707e403f6fa4160aa7.tar.xz ida-tui-82d5151c58ea6ed5681525707e403f6fa4160aa7.zip | |
Build the listing index in one call: boot 9.3s -> 1.25s, 911 calls -> 4
The listing used to learn its shape by fetching it. Even after skeleton pages
that was 457 round trips and 227k rows for a 1.2MB bash, to end up knowing
how many rows there are and where each one is.
segment_index(detail=True) now returns exactly that -- every row's address,
kind and size as packed arrays, plus the page boundaries -- from one walk that
builds no rows and renders no text. ListingModel.build_from_index() decodes it
straight into _heads/_head_eas/_row_at/_by_ea/_page_*, marks every row
_SKELETON_GEN, and declares itself complete. _grow has nothing left to stream.
Nothing else in the model changed, because a row without text is a state it
already had: the FIRST read of a page materialises it through the same
_ensure_text/_ensure_page path a rename uses. That is why this is a ~90 line
change to a core view rather than a rewrite.
bash boot: 911 calls / 9.26s -> 4 calls / 1.25s 7.4x
whole census (boot + 9 UI actions): 933 calls -> 30
Two things had to be exactly right, and both are tested rather than argued:
* the ROW COUNT, or the scrollbar lies. Verified equal to a fully streamed
model, and every row's ea/kind/size equal too, 228,659 of them, zero
mismatches.
* the PAGE BOUNDARIES, or _ensure_page refetches a page that does not line
up, fails its structure check and triggers a full rebuild. heads() pages on
PHYSICAL rows; anchoring every N LOGICAL rows looks identical (the two only
diverge once a segment holds an undefined run) and would have been a
lurking bug on .bss. Anchors now carry [logical_row, ea, head_index] taken
at the real boundary, and are asserted equal to the streamer's own.
Transport note: the packed arrays are base64, not raw bytes. _PACK_EPILOGUE
serialises with json.dumps(default=str), which turns bytes into their repr --
2.97MB arrived as 11.26MB of unparseable text before that was spotted.
The new test builds both models back to back and compares every internal
array. An earlier version compared against the app's long-lived model and was
off by one row, because scenarios before it rename and define things: that
model describes the database at boot, not now.
Full gate: 1063 passed, twice.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_scenarios.py | 32 |
1 files changed, 32 insertions, 0 deletions
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index b256b80..0bbcf17 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -459,6 +459,38 @@ async def s_segment_index(c: Ctx): c.check("every anchor points at the row it claims", not bad, f"{len(bad)} wrong, first={bad[:2]}") + # A model built from the index must be INDISTINGUISHABLE from a streamed + # one. That is the invariant the whole optimisation rests on: _prime builds + # from the index now, and every read path -- rendering, goto, xrefs, search, + # rename refresh -- indexes into these arrays assuming they were produced + # the old way. Comparing row counts alone would miss a shifted _row_at or a + # _by_ea that sends a jump to the wrong line. + # + # BOTH models are built here, back to back. Comparing against the app's + # long-lived model instead is wrong by one row and flaky: earlier scenarios + # rename and define things, so that model describes the database as it was + # at boot, not as it is now. + from idatui.domain import ListingModel # noqa: PLC0415 + args = (app.program, model.seg_start, model.seg_end, model.name) + idx_model, streamed = ListingModel(*args), ListingModel(*args) + if not idx_model.build_from_index(): + c.check("build_from_index works", False) + return + while not streamed.complete: + if streamed.load_next_page(text=False) == 0: + break + c.check("an index-built model is complete immediately", idx_model.complete) + c.check("index-built model has the streamed row count", + len(idx_model) == len(streamed), f"{len(idx_model)} vs {len(streamed)}") + for field in ("_row_at", "_head_eas", "_by_ea", "_page_head", + "_page_addr", "_page_rows"): + a, b = getattr(idx_model, field), getattr(streamed, field) + c.check(f"index-built {field} matches streaming", a == b, + f"len {len(a)} vs {len(b)}") + c.check("index-built rows carry the same ea/kind/size", + [(h.ea, h.kind, h.size) for h in idx_model._heads] + == [(h.ea, h.kind, h.size) for h in streamed._heads]) + @scenario("skeleton_pages") async def s_skeleton_pages(c: Ctx): |
