aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--experiments/profile_client.py71
-rw-r--r--idatui/domain.py28
2 files changed, 90 insertions, 9 deletions
diff --git a/experiments/profile_client.py b/experiments/profile_client.py
new file mode 100644
index 0000000..77f16e7
--- /dev/null
+++ b/experiments/profile_client.py
@@ -0,0 +1,71 @@
+"""Profile the CLIENT half of streaming a segment.
+
+`profile_remote.py` profiles inside the database process. This one profiles the
+other side: unpickling a page, building Heads and maintaining the model's
+indexes. Once the backend got cheap that half became the majority of boot, and
+nothing else here can see it.
+
+ PYTHONPATH=. ~/ida-venv/bin/python experiments/profile_client.py [BINARY] [--pages N]
+
+Time spent in `invoke` is the backend + transport; everything below it in the
+`tottime` list is ours and is what this file is for.
+"""
+from __future__ import annotations
+
+import argparse
+import cProfile
+import io
+import os
+import pstats
+import time
+
+from idatui.codemode_client import CodeModeClient
+from idatui.domain import Program
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser()
+ ap.add_argument("binary", nargs="?", default="targets/bash")
+ ap.add_argument("--pages", type=int, default=60)
+ ap.add_argument("--lines", type=int, default=16)
+ ap.add_argument("--text", action="store_true",
+ help="load full pages instead of skeletons")
+ args = ap.parse_args()
+
+ client = CodeModeClient(os.path.abspath(args.binary))
+ client.connect()
+ program = Program(client)
+ regions = client.invoke("file_regions")
+ rows = regions.get("regions") or regions.get("result") or []
+ text_seg = next((r for r in rows if ".text" in str(r.get("name", ""))), rows[0])
+ model = program.listing(int(str(text_seg["start"]), 16))
+ assert model is not None
+ model.load_next_page() # prime one page, and install the remote lib
+
+ want_text = bool(args.text)
+ pr = cProfile.Profile()
+ started = time.perf_counter()
+ pr.enable()
+ loaded = 0
+ for _ in range(args.pages):
+ if model.complete:
+ break
+ n = model.load_next_page(text=want_text)
+ if n == 0:
+ break
+ loaded += 1
+ pr.disable()
+ wall = (time.perf_counter() - started) * 1000
+
+ print(f"# {os.path.basename(args.binary)} pages={loaded} "
+ f"text={want_text} {wall:.0f}ms ({wall/max(loaded,1):.2f}ms/page)")
+ buf = io.StringIO()
+ pstats.Stats(pr, stream=buf).sort_stats("tottime").print_stats(args.lines)
+ print(buf.getvalue())
+ program.close()
+ client.close()
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/idatui/domain.py b/idatui/domain.py
index 903611f..60dbbbf 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -45,9 +45,12 @@ _TRUNC_RE = re.compile(r"\[(\d+) chars total\]\s*$")
# Value models
# --------------------------------------------------------------------------- #
def _as_int(v) -> int:
+ # Both arms of the ternary this used to end with were `int(v, 16)`, so the
+ # isinstance+startswith test in front of them decided nothing and ran on
+ # every address the client parses -- 55k times per 60 listing pages.
if isinstance(v, int):
return v
- return int(v, 16) if isinstance(v, str) and v.startswith("0x") else int(v, 16)
+ return int(v, 16)
@dataclass(frozen=True)
@@ -140,15 +143,22 @@ class Head(NamedTuple):
# tool emits [str, str] and [int, int, int], so re-coercing them was
# re-proving that once per listing row -- and copying them into tuples
# destroyed the sharing the worker's line cache had just created.
+ #
+ # Built POSITIONALLY, and with the address converted inline. This is the
+ # most-constructed object in the codebase (227k of them to stream one
+ # bash) and the two together are worth ~40%: keyword construction has to
+ # match names against the tuple's fields, and _as_int was a call per row
+ # to do one isinstance and an int().
+ v = d["ea"]
return cls(
- ea=_as_int(d["ea"]),
- kind=d.get("kind", "unknown"),
- size=int(d.get("size", 0) or 0),
- text=d.get("text", ""),
- name=d.get("name"),
- raw=raw,
- spans=d.get("spans") or None,
- ops=d.get("ops") or None,
+ v if isinstance(v, int) else int(v, 16),
+ d.get("kind", "unknown"),
+ int(d.get("size", 0) or 0),
+ d.get("text", ""),
+ d.get("name"),
+ raw,
+ d.get("spans") or None,
+ d.get("ops") or None,
)