aboutsummaryrefslogtreecommitdiffstats
path: root/.auto/parked
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-07 09:13:33 +0200
committeruser <user@clank>2026-08-07 09:13:33 +0200
commit0fc0b3a9d667f7f464939bce8bfdf4cd9cd889bb (patch)
treef8c30d42e79b683b2f7770f2caeecbedcaab22ae /.auto/parked
parentdecomp_map: memoise obj_id -> ea for the whole function instead of only compa... (diff)
downloadida-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.py110
-rw-r--r--.auto/parked/fast_decompile.patch122
-rw-r--r--.auto/parked/fast_pc_nums.patch52
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"]),