summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-07 06:58:47 +0200
committeruser <user@clank>2026-08-07 06:58:47 +0200
commit7e4f0869a0aeffc155e9153b654c5880b2de0d86 (patch)
treebabb9866a1c8c8007cf7feacc74d338be8d25baa /server
parentheads(digest=True): the worker answers "does this page still render exactly a... (diff)
downloadida-tui-7e4f0869a0aeffc155e9153b654c5880b2de0d86.tar.gz
ida-tui-7e4f0869a0aeffc155e9153b654c5880b2de0d86.tar.xz
ida-tui-7e4f0869a0aeffc155e9153b654c5880b2de0d86.zip
Three redundancies in the heads walk: item flags were fetched three times per head (row builder, _is_unknown via _advance, and _rows_for), get_func was called per head where a head is nearly always in the same function as the one before it, and the page digest rebuilt a tuple-of-tuples per row where one spans list is shared by ~45% of them. Cold heads 18.62 -> 17.77 us/row, warm 11.53 -> 10.87.
Result: {"status":"keep","total_ms":25814,"lg_boot_ms":714.9,"lg_decomp_ms":2376.2,"lg_graph_ms":948.8,"lg_hex_ms":458.6,"lg_index_ms":69.3,"lg_listing_cold_ms":448.5,"lg_listing_warm_ms":482.1,"lg_nav_ms":6808,"lg_palette_ms":4.7,"lg_rename_ms":752,"lg_render_ms":219.4,"lg_search_ms":4008.3,"lg_split_ms":2275.5,"pure_graph_ms":225.2,"sm_boot_ms":456.1,"sm_decomp_ms":1292.4,"sm_graph_ms":795.8,"sm_hex_ms":475.2,"sm_index_ms":2.5,"sm_listing_cold_ms":269.1,"sm_listing_warm_ms":272.2,"sm_nav_ms":320.2,"sm_palette_ms":0.3,"sm_rename_ms":446.3,"sm_render_ms":264.8,"sm_search_ms":60.6,"sm_split_ms":1366.9,"fails":0}
Diffstat (limited to 'server')
-rw-r--r--server/patch_server.py66
1 files changed, 50 insertions, 16 deletions
diff --git a/server/patch_server.py b/server/patch_server.py
index a34a4a9..a991158 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -282,14 +282,19 @@ def read_raw(
return {"addr": addr, "hex": bytes(ba).hex(), "n": len(ba)}
-def _idatui_head_row(ea):
+def _idatui_head_row(ea, flags=None):
"""One flat-listing row for the head at ``ea``: kind (code/data/unknown),
- byte size, rendered text, and any symbol name."""
+ byte size, rendered text, and any symbol name.
+
+ ``flags`` lets a caller that already asked for them say so -- the walk in
+ ``heads`` used to fetch them three times per head (here, in _is_unknown from
+ _advance, and again from _rows_for).
+ """
import ida_bytes
import ida_lines
import ida_name
- f = ida_bytes.get_flags(ea)
+ f = ida_bytes.get_flags(ea) if flags is None else flags
if ida_bytes.is_code(f):
kind = "code"
elif ida_bytes.is_data(f):
@@ -590,11 +595,23 @@ def _idatui_rows_digest(rows):
hashes to that. One worker, one process, one hash seed.
"""
acc = 0
+ # The per-line render is memoised, so one spans list is shared by every row
+ # that says the same thing -- about 45% of them within a page. Hash each
+ # distinct list once and key that by identity, rather than rebuilding a
+ # tuple of tuples per row (which is the exact cost that was measured and
+ # removed from the client side for the same reason).
+ seen = {}
for r in rows:
sp = r.get("spans")
+ if sp is None:
+ sh = None
+ else:
+ key = id(sp)
+ sh = seen.get(key)
+ if sh is None:
+ sh = seen[key] = hash(tuple(map(tuple, sp)))
acc = hash((acc, r.get("ea"), r.get("kind"), r.get("size"),
- r.get("text"), r.get("name"),
- tuple(map(tuple, sp)) if sp else None))
+ r.get("text"), r.get("name"), sh))
return acc
@@ -735,8 +752,7 @@ def heads(
# by get_item_end; a run of undefined bytes is COLLAPSED into one row (its
# end found in O(1) via next_head, which skips undefined) so a large .bss or
# gap doesn't explode into millions of one-byte rows.
- def _is_unknown(e):
- f = ida_bytes.get_flags(e)
+ def _is_unknown_f(f):
return not (ida_bytes.is_code(f) or ida_bytes.is_data(f))
def _run_end(e):
@@ -744,21 +760,37 @@ def heads(
nh = ida_bytes.next_head(e, hi)
return nh if (nh != idaapi.BADADDR and e < nh <= hi) else hi
- def _advance(e):
- if _is_unknown(e):
+ def _advance(e, f):
+ if _is_unknown_f(f):
return _run_end(e)
nxt = ida_bytes.get_item_end(e)
return nxt if nxt > e else e + 1
- def _rows_for(e):
- if _is_unknown(e):
+ # The function the walk is currently inside, reused while it stays inside.
+ # get_func is ~0.5us and the walk asks per head; a head is nearly always in
+ # the same function as the one before it. Only ever consulted when ``e``
+ # falls in [start_ea, end_ea), so a tail chunk elsewhere cannot be
+ # misattributed -- checked against get_func over 437k heads of
+ # bash/ls_ttl/echo with zero disagreements.
+ fn_cache = [None]
+
+ def _func_at(e):
+ cur = fn_cache[0]
+ if cur is not None and cur.start_ea <= e < cur.end_ea:
+ return cur
+ cur = idaapi.get_func(e)
+ fn_cache[0] = cur
+ return cur
+
+ def _rows_for(e, f):
+ if _is_unknown_f(f):
return [_idatui_unknown_row(e, _run_end(e) - e)]
- func = idaapi.get_func(e) if annotate else None
+ func = _func_at(e) if annotate else None
at_start = func is not None and func.start_ea == e
out = []
if at_start:
out.extend(_idatui_func_header_rows(e))
- row = _idatui_head_row(e)
+ row = _idatui_head_row(e, f)
if at_start:
row = dict(row)
row["name"] = None # the name is shown on the proc header line
@@ -778,17 +810,19 @@ def heads(
return out
ea = ida_bytes.get_item_head(start)
+ get_flags = ida_bytes.get_flags
for _ in range(offset):
if ea >= hi or ea == idaapi.BADADDR:
break
- ea = _advance(ea)
+ ea = _advance(ea, get_flags(ea))
more = False
while ea != idaapi.BADADDR and ea < hi:
if len(rows) >= count:
more = True
break
- rows.extend(_rows_for(ea)) # a struct head expands into member rows
- ea = _advance(ea)
+ f = get_flags(ea) # once per head, not once per consumer
+ 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)}