aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <peter@haxx.in>2026-08-07 08:17:07 +0200
committerblasty <peter@haxx.in>2026-08-07 08:17:07 +0200
commitefc633afd4f8b0163a23aba0c61f83760e952422 (patch)
tree971d8e9ffbb0d3e60818322ea13f12a8b76a781e
parentAn item edit (c/d/u/p) keeps the listing's walk in front of it instead of dis... (diff)
downloadida-tui-efc633afd4f8b0163a23aba0c61f83760e952422.tar.gz
ida-tui-efc633afd4f8b0163a23aba0c61f83760e952422.tar.xz
ida-tui-efc633afd4f8b0163a23aba0c61f83760e952422.zip
The page-freshness check carries the digest the client already holds (heads(expect=...)) instead of asking first and fetching afterwards. A page that has NOT changed costs one round trip as before; a page that HAS changed now costs one instead of two. Also corrects the record: the item-edit bench hang is pilot start-up flakiness, not the _prime/_grow concurrency I blamed it on — proved with a stack dump.
Result: {"status":"keep","total_ms":25121.9,"lg_boot_ms":694.8,"lg_decomp_ms":2370.8,"lg_graph_ms":917.1,"lg_hex_ms":456.1,"lg_index_ms":69.4,"lg_listing_cold_ms":442.7,"lg_listing_warm_ms":498.8,"lg_nav_ms":6548.8,"lg_palette_ms":4.8,"lg_rename_ms":711.4,"lg_render_ms":224.9,"lg_search_ms":3484.7,"lg_split_ms":2619.3,"pure_graph_ms":220.7,"sm_boot_ms":448.6,"sm_decomp_ms":1292.5,"sm_graph_ms":748.8,"sm_hex_ms":438.3,"sm_index_ms":2.4,"sm_listing_cold_ms":274.3,"sm_listing_warm_ms":266.5,"sm_nav_ms":302.7,"sm_palette_ms":0.3,"sm_rename_ms":392.5,"sm_render_ms":260.2,"sm_search_ms":59.6,"sm_split_ms":1370.9,"fails":0}
-rw-r--r--idatui/domain.py36
-rw-r--r--server/patch_server.py24
2 files changed, 30 insertions, 30 deletions
diff --git a/idatui/domain.py b/idatui/domain.py
index 2cfd9d9..aedade9 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -1011,30 +1011,26 @@ class ListingModel:
want_digest = self._page_digest[p]
want_rows = self._page_rows[p]
want = [(h.ea, h.kind) for h in self._heads[lo:hi]]
- # Ask whether the page still renders as the client holds it. The worker
- # builds the rows either way (there is no knowing a line is unchanged
- # without rendering it), but skipping the pickling, the transfer, the
- # unpickling and the Head rebuild is about 40% of what a page costs --
- # and after a rename almost every page is unchanged.
- if want_digest is not None:
- try:
- probe = self._prog.client.call(
- "heads", addr=hex(addr), count=self.PAGE, annotate=True,
- digest=True)
- except Exception: # noqa: BLE001 -- an older worker has no digest
- probe = None
- if (isinstance(probe, dict) and probe.get("digest") == want_digest
- and probe.get("count") == want_rows):
- with self._lock:
- if self._text_gen == gen and len(self._heads) >= hi:
- for k in range(lo, hi):
- self._head_gen[k] = gen
- return p + 1
+ # Tell the worker what we already hold. It builds the rows either way
+ # (there is no knowing a line is unchanged without rendering it), but if
+ # they still hash to the same value it keeps them: the pickling, the
+ # transfer, the unpickling and the Head rebuild are about 40% of what a
+ # page costs, and after a rename almost every page is unchanged. Sending
+ # the expectation rather than asking first means a page that HAS changed
+ # still costs one round trip.
try:
payload = self._prog.client.call(
- "heads", addr=hex(addr), count=self.PAGE, annotate=True)
+ "heads", addr=hex(addr), count=self.PAGE, annotate=True,
+ expect="" if want_digest is None else str(want_digest))
except Exception: # noqa: BLE001 -- keep the old text rather than blank
return p + 1
+ if (isinstance(payload, dict) and "heads" not in payload
+ and payload.get("count") == want_rows):
+ with self._lock:
+ if self._text_gen == gen and len(self._heads) >= hi:
+ for k in range(lo, hi):
+ self._head_gen[k] = gen
+ return p + 1
rows = payload.get("heads", []) if isinstance(payload, dict) else []
page = self._build_page(rows)
with self._lock:
diff --git a/server/patch_server.py b/server/patch_server.py
index a991158..e860676 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -700,7 +700,7 @@ def heads(
end: Annotated[str, "Optional exclusive end address; default = segment end"] = "",
back: Annotated[bool, "Walk backwards: return the count heads ENDING just before addr, in forward order"] = False,
annotate: Annotated[bool, "Emit IDA-style function boundary banner rows (kind sep/funchdr)"] = False,
- digest: Annotated[bool, "Return only a digest+count of the rows, not the rows themselves"] = False,
+ expect: Annotated[str, "Digest a caller already holds: the rows are omitted when they still hash to it"] = "",
) -> dict:
"""Walk item heads from ``addr`` as a flat listing: every head is rendered
(code OR data OR undefined) via generate_disasm_line and stepped with
@@ -824,15 +824,19 @@ def heads(
rows.extend(_rows_for(ea, f)) # a struct head expands into member rows
ea = _advance(ea, f)
cursor = {"next": hex(ea)} if more else {"done": True}
- out = {"addr": str(addr), "cursor": cursor,
- "digest": _idatui_rows_digest(rows), "count": len(rows)}
- # ``digest`` mode answers "is this page still exactly what you have?" without
- # shipping it. The rows are built either way -- generate_disasm_line is the
- # floor and there is no way to know a line is unchanged without rendering it
- # -- but pickling several hundred rows with their colour spans, unpickling
- # them and rebuilding Heads is about 40% of what a page costs, and after a
- # rename almost every page comes back identical.
- if not digest:
+ dig = _idatui_rows_digest(rows)
+ out = {"addr": str(addr), "cursor": cursor, "digest": dig, "count": len(rows)}
+ # ``expect`` says "I already hold a page that hashed to this". The rows are
+ # built either way -- generate_disasm_line is the floor and there is no way
+ # to know a line is unchanged without rendering it -- but pickling several
+ # hundred rows with their colour spans, unpickling them and rebuilding Heads
+ # is about 40% of what a page costs, and after a rename almost every page
+ # comes back identical.
+ #
+ # It carries the expected value rather than being a yes/no "digest mode" so
+ # that a page which HAS changed still costs one round trip: asking first and
+ # fetching afterwards made every changed page two.
+ if not (expect and str(dig) == expect):
out["heads"] = rows
return out