diff options
| author | user <user@clank> | 2026-08-07 09:13:33 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-07 09:13:33 +0200 |
| commit | 0fc0b3a9d667f7f464939bce8bfdf4cd9cd889bb (patch) | |
| tree | f8c30d42e79b683b2f7770f2caeecbedcaab22ae /.auto/parked | |
| parent | decomp_map: memoise obj_id -> ea for the whole function instead of only compa... (diff) | |
| download | ida-tui-0fc0b3a9d667f7f464939bce8bfdf4cd9cd889bb.tar.gz ida-tui-0fc0b3a9d667f7f464939bce8bfdf4cd9cd889bb.tar.xz ida-tui-0fc0b3a9d667f7f464939bce8bfdf4cd9cd889bb.zip | |
Park two proven-but-unresolvable F5-path optimisations, and record how to spot a counter-drift outlier
pc_nums allocated three ctree_item_t SWIG objects per candidate column -- the
third instance of the same fault already fixed in decomp_map and found in
decompile_function_safe. 1247 -> 614ms warm over 18991 lines of bash, identical
literal counts, 0 mismatches over echo's 128 functions.
Both it and the fast decompile_function_safe are parked rather than applied:
together they are worth ~350ms against a run-to-run spread of ~500ms on this
box (means 25045 with, 25140 without over seven runs), so the benchmark cannot
resolve them. Neither adds complexity -- both remove allocations -- so they are
kept on disk with their measurements for a per-operation-latency goal.
Also records the 27283ms outlier: the work counters moved with it, which is how
an outlier is told from a regression.
Diffstat (limited to '.auto/parked')
| -rw-r--r-- | .auto/parked/check_decomp.py | 110 | ||||
| -rw-r--r-- | .auto/parked/fast_decompile.patch | 122 | ||||
| -rw-r--r-- | .auto/parked/fast_pc_nums.patch | 52 |
3 files changed, 284 insertions, 0 deletions
diff --git a/.auto/parked/check_decomp.py b/.auto/parked/check_decomp.py new file mode 100644 index 0000000..0f30a03 --- /dev/null +++ b/.auto/parked/check_decomp.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Differential gate for the fast decompile_function_safe (run by checks.sh). + +`idatui/worker.py` rebinds ida-pro-mcp's `decompile_function_safe` to our own +loop, which skips two of the three SWIG allocations per line and memoises the +per-line `dstr()` by ctree obj_id. That is a pure speed change and the text it +returns is what the pseudocode pane shows, markers and all -- so it has to be +byte-identical, not merely similar. + +This runs BOTH implementations against the same cfunc for every function of a +real binary and compares the strings, with `include_addresses` both ways (the +marker path is the whole point, and the no-marker path must not regress either). + + ~/ida-venv/bin/python .auto/check_decomp.py [targets/echo] [max_funcs] +""" +from __future__ import annotations + +import os +import shutil +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +sys.path.insert(0, ROOT) +sys.path.insert(0, HERE) + +from bench import stage # noqa: E402 + + +def _add_mcp_path() -> None: + """ida-pro-mcp is installed for the interpreter the WORKER runs, which is + not necessarily the one running this check.""" + import glob + for pat in ("/home/user/.local/lib/python3.*/site-packages", + os.path.expanduser("~/.local/lib/python3.*/site-packages")): + for d in glob.glob(pat): + if os.path.isdir(os.path.join(d, "ida_pro_mcp")) and d not in sys.path: + sys.path.append(d) + + +def main() -> int: + target = sys.argv[1] if len(sys.argv) > 1 else "targets/echo" + limit = int(sys.argv[2]) if len(sys.argv) > 2 else 400 + d, path = stage(os.path.join(ROOT, target)) + os.environ["IDA_MCP_TOOL_TIMEOUT_SEC"] = "0" + _add_mcp_path() + try: + import idapro + idapro.open_database(path, run_auto_analysis=True) + try: + import ida_funcs + import ida_hexrays + import idautils + from ida_pro_mcp.ida_mcp import utils + + original = utils.decompile_function_safe + sys.path.insert(0, os.path.join(ROOT, "server")) + import patch_server + # exec only our function out of the decoded BODY: the rest of it + # needs api_types' namespace (@tool, @idasync, ...). + body = patch_server.BODY + i = body.find("def _idatui_decompile_function_safe(") + j = body.find("\n_idatui_strings_cache", i) + assert i > 0 and j > i, "could not slice the function out of BODY" + g = {} + exec(body[i:j], g) + fast = g["_idatui_decompile_function_safe"] + + ida_hexrays.init_hexrays_plugin() + fails: list[str] = [] + n = ok = 0 + for ea in idautils.Functions(): + f = ida_funcs.get_func(ea) + if not f: + continue + n += 1 + if n > limit: + break + for markers in (True, False): + a, ea_err = original(f.start_ea, include_addresses=markers) + b, eb_err = fast(f.start_ea, include_addresses=markers) + if a != b or (ea_err is None) != (eb_err is None): + fails.append(f"{f.start_ea:#x} (markers={markers})") + if len(fails) <= 3: + la = (a or "").splitlines() + lb = (b or "").splitlines() + i = next((k for k, (x, y) in enumerate(zip(la, lb)) + if x != y), None) + if i is None: + print(f" FAIL {f.start_ea:#x}: {len(la)} lines " + f"vs {len(lb)}, errs {ea_err!r}/{eb_err!r}") + else: + print(f" FAIL {f.start_ea:#x} line {i}:") + print(f" original: {la[i][:100]!r}") + print(f" fast : {lb[i][:100]!r}") + break + else: + ok += 1 + print(f"decompile text: {ok}/{min(n, limit)} functions of {target} " + f"byte-identical to ida-pro-mcp's own loop, " + f"{len(fails)} problems") + return 1 if fails else 0 + finally: + idapro.close_database(save=False) + finally: + shutil.rmtree(d, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.auto/parked/fast_decompile.patch b/.auto/parked/fast_decompile.patch new file mode 100644 index 0000000..c5a4ceb --- /dev/null +++ b/.auto/parked/fast_decompile.patch @@ -0,0 +1,122 @@ +diff --git a/idatui/worker.py b/idatui/worker.py +index 556e69a..04252cc 100644 +--- a/idatui/worker.py ++++ b/idatui/worker.py +@@ -119,6 +119,36 @@ def recv(sock: socket.socket): + # --------------------------------------------------------------------------- # + # worker + # --------------------------------------------------------------------------- # ++def _use_fast_decompile() -> None: ++ """Point ida-pro-mcp's decompile tools at our per-line loop. ++ ++ The shipped ``decompile_function_safe`` allocates three ctree_item_t SWIG ++ objects per pseudocode line (two of which it never reads) and formats an ++ item description per line to recover the ``/*0xEA*/`` marker: 121us a line, ++ which on a warm cfunc is most of what the tool costs. The replacement lives ++ in server/patch_server.py and is differentially checked against the original ++ by .auto/check_decomp.py. ++ ++ Every consumer binds the name at import time (``from .utils import ...``), ++ so rebinding it on ``utils`` alone would miss them; rebind on each module ++ that imported it, and leave anything unexpected exactly as it was. ++ """ ++ try: ++ from ida_pro_mcp.ida_mcp import api_types, utils ++ fast = api_types._idatui_decompile_function_safe ++ except Exception: # noqa: BLE001 -- never let this stop the worker booting ++ return ++ utils.decompile_function_safe = fast ++ # Rebind only on modules that are ALREADY imported. Importing one to rebind ++ # it would be work the worker had not chosen to do, on the boot path. ++ prefix = "ida_pro_mcp.ida_mcp." ++ for name, mod in list(sys.modules.items()): ++ if not name.startswith(prefix) or mod is None: ++ continue ++ if getattr(mod, "decompile_function_safe", None) is not None: ++ mod.decompile_function_safe = fast ++ ++ + def _ensure_tools_injected() -> None: + """Inject idatui's custom tools (heads/read_raw/resolve_names/func_types/...) + into the installed ida_pro_mcp, idempotently, so the worker is self-sufficient +@@ -190,6 +220,8 @@ def _open_and_register(binpath: str, load_args: str = ""): + # importing the package registers all api_*/patched tools against MCP_SERVER + from ida_pro_mcp.ida_mcp import MCP_SERVER # noqa: WPS433 + ++ _use_fast_decompile() ++ + import ida_nalt + module = os.path.basename(ida_nalt.get_root_filename() or binpath) + +diff --git a/server/patch_server.py b/server/patch_server.py +index 6667e12..64424e4 100644 +--- a/server/patch_server.py ++++ b/server/patch_server.py +@@ -1039,6 +1039,67 @@ def decomp_map( + return {"addr": hex(func.start_ea), "lines": lines} + + ++def _idatui_decompile_function_safe(ea, include_addresses=True): ++ """ida-pro-mcp's ``decompile_function_safe``, with the three costs the same ++ sweep had in ``decomp_map`` taken out. Byte-identical output -- it is ++ differentially checked against the original over every function of a real ++ binary by ``.auto/check_decomp.py``. ++ ++ The shipped version costs 121us per pseudocode line, which is more than the ++ line's share of Hex-Rays itself on a warm cfunc: ++ ++ * it allocates THREE ctree_item_t SWIG objects per line, and ``_head`` and ++ ``_tail`` are never read -- ``get_line_item`` accepts None for both. ++ * it calls ``dstr()`` per line. That formats a whole 'EA: description' ++ string at 24us a call, and consecutive lines of a multi-line expression ++ report the same ctree item, so memoising by ``obj_id`` (unique within a ++ cfunc) skips most of them. ++ ++ 18 991 lines of bash: 2 302ms -> 429ms. ++ """ ++ import ida_lines ++ import ida_hexrays as _hx ++ from ida_pro_mcp.ida_mcp.utils import compact_whitespace, decompile_checked ++ from ida_pro_mcp.ida_mcp.sync import IDAError ++ try: ++ cfunc = decompile_checked(ea) ++ item = _hx.ctree_item_t() ++ get_line_item = cfunc.get_line_item ++ tag_remove = ida_lines.tag_remove ++ ea_of_id = {} ++ lines = [] ++ for sl in cfunc.get_pseudocode(): ++ line = sl.line ++ line_ea = None ++ if include_addresses and get_line_item(line, 0, False, None, ++ item, None): ++ it = item.it ++ oid = it.obj_id if it is not None else None ++ if oid is not None and oid in ea_of_id: ++ line_ea = ea_of_id[oid] ++ else: ++ dstr = item.dstr() ++ if dstr: ++ ds = dstr.split(": ") ++ if len(ds) == 2: ++ try: ++ line_ea = int(ds[0], 16) ++ except ValueError: ++ pass ++ if oid is not None: ++ ea_of_id[oid] = line_ea ++ text = compact_whitespace(tag_remove(line)) ++ if line_ea is not None: ++ lines.append(f"{text} /*{line_ea:#x}*/") ++ else: ++ lines.append(text) ++ return "\\n".join(lines), None ++ except IDAError as e: ++ return None, str(e) ++ except Exception as e: ++ return None, f"Decompilation failed at {hex(ea)}: {e}" ++ ++ + _idatui_strings_cache = {} + + diff --git a/.auto/parked/fast_pc_nums.patch b/.auto/parked/fast_pc_nums.patch new file mode 100644 index 0000000..f06bfe1 --- /dev/null +++ b/.auto/parked/fast_pc_nums.patch @@ -0,0 +1,52 @@ +diff --git a/server/patch_server.py b/server/patch_server.py +index 6667e12..5e20671 100644 +--- a/server/patch_server.py ++++ b/server/patch_server.py +@@ -1900,7 +1900,7 @@ def _idatui_lit_extent(plain, x): + return (lo, hi) + + +-def _idatui_pc_nums(cf, sl): ++def _idatui_pc_nums(cf, sl, plain=None): + """Every number literal on one pseudocode line, as + [{x0, x1, ea, opnum, value, nbytes, fmt}]. + +@@ -1912,16 +1912,26 @@ def _idatui_pc_nums(cf, sl): + import ida_lines + import idaapi + +- plain = ida_lines.tag_remove(sl.line) ++ # ``plain`` is the untagged line; callers that already have it pass it in ++ # rather than making tag_remove run twice over every line of the function. ++ if plain is None: ++ plain = ida_lines.tag_remove(sl.line) + out = [] + x = 0 ++ # One ctree_item_t for the whole line, and no head/tail at all. They are ++ # SWIG allocations in the innermost loop of a scan that probes every ++ # literal-looking character -- and 'a' to 'f' are hex digits, so `a1`, `v6` ++ # and `sub_1F4C0` all qualify and most columns of a line get probed. head ++ # and tail were never read. (Same three costs as decomp_map's sweep.) ++ item = ida_hexrays.ctree_item_t() ++ line = sl.line ++ get_line_item = cf.get_line_item + while x < len(plain): + ch = plain[x] + if ch not in _IDATUI_LIT_CHARS and ch != "'": + x += 1 + continue +- head, item, tail = (ida_hexrays.ctree_item_t() for _ in range(3)) +- if not cf.get_line_item(sl.line, x, True, head, item, tail): ++ if not get_line_item(line, x, True, None, item, None): + x += 1 + continue + if item.citype != ida_hexrays.VDI_EXPR: +@@ -1997,7 +2007,7 @@ def pc_nums( + for i in range(len(sv)): + plain = ida_lines.tag_remove(sv[i].line) + compact = _idatui_compact(plain) +- for rec in _idatui_pc_nums(cf, sv[i]): ++ for rec in _idatui_pc_nums(cf, sv[i], plain): + out.append({ + "line": i, + "x0": _idatui_compact_col(plain, compact, rec["x0"]), |
