diff options
| -rw-r--r-- | experiments/profile_client.py | 71 | ||||
| -rw-r--r-- | idatui/domain.py | 28 |
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, ) |
