diff options
| author | blasty <blasty@local> | 2026-08-09 22:50:31 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-08-09 22:50:31 +0200 |
| commit | 4d490052f7d4dc0fccb3e718c801ef99e8646e22 (patch) | |
| tree | 5b4cb17c9ac59978f14fb7351b229a19fb60d0b4 | |
| parent | Docs: re-check ALL nine upstream findings against 0.3.2 (diff) | |
| download | ida-tui-4d490052f7d4dc0fccb3e718c801ef99e8646e22.tar.gz ida-tui-4d490052f7d4dc0fccb3e718c801ef99e8646e22.tar.xz ida-tui-4d490052f7d4dc0fccb3e718c801ef99e8646e22.zip | |
SPEED: replace the historical backend table with a real 0.3.1 vs 0.3.2 A/B
The worker-vs-Code-Mode table was measured before 0.3.2 and with both
workarounds active, so it answered a question nobody asks any more. Replaced
with three configurations measured on the same box, rolling both checkouts
back and forward:
A old client WITH workarounds on 0.3.1 -- what shipped
B current client on 0.3.1 -- what the workarounds were for
C current client on 0.3.2 -- now
Headline: the real-world gain is ~1.4x geomean, NOT the 6.9x the empty round
trip advertises, and the doc says so in those words -- because the tempting
number to quote is the wrong one. The A->C vs B->C gap is the actual story:
stock 0.3.1 was 5.4x slower, so the workarounds had already recovered nearly
everything and upstream mostly bought us the right to delete them.
Also records the three cost classes (payload- / round-trip- / IDA-dominated)
so the next person optimising here knows which lever moves which op, and the
~10% run-to-run spread so a sub-1.2x 'regression' doesn't start a hunt.
Old worker table kept below, labelled historical. experiments/bench_ops.py is
the harness, with the copy-to-/tmp-before-checkout trick documented in it.
| -rw-r--r-- | .fastfeedback/SPEED.md | 60 | ||||
| -rw-r--r-- | experiments/bench_ops.py | 103 |
2 files changed, 155 insertions, 8 deletions
diff --git a/.fastfeedback/SPEED.md b/.fastfeedback/SPEED.md index 8791768..fb2a171 100644 --- a/.fastfeedback/SPEED.md +++ b/.fastfeedback/SPEED.md @@ -203,19 +203,66 @@ includes the real work each key triggers, since `press` settles), 2.9s settling, 2.1s waits, ~5s scenario bodies. The rest of the gate is dominated by per-suite IDA boot — 7 processes, each opening its own database. -### Backend performance (worker vs Code Mode), after the two transport fixes +### Backend performance: ida-codemode 0.3.1 vs 0.3.2 -| op | worker | codemode | | +Measured `targets/bash`, function `main` (`0x1ca40`), 20 reps, medians, same box, +rolling **both** checkouts back and forward. Harness: `experiments/bench_ops.py` +(copy it to /tmp first — a `git checkout` of an old commit would replace it). + +Three configurations, because comparing the wrong pair gives the wrong answer: + +- **A** = old client **with** its settrace+packing workarounds, on 0.3.1 — *what shipped* +- **B** = current client (workarounds removed) on 0.3.1 — *what the workarounds were for* +- **C** = current client on 0.3.2 — *now* + +| op | A shipped | C now | A→C | B stock 0.3.1 | B→C | +|---|---|---|---|---|---| +| empty round trip | 2.06ms | 0.30ms | **6.90x** | 2.11ms | 7.05x | +| `list_funcs` 500 | 17.55ms | 15.46ms | 1.14x | 260.47ms | **16.85x** | +| `heads` 200 (listing page) | 6.45ms | 4.38ms | 1.47x | 26.40ms | 6.02x | +| `decompile` (warm) | 34.04ms | 32.09ms | 1.06x | 107.52ms | 3.35x | +| `flowchart` (graph) | 8.13ms | 6.18ms | 1.32x | 38.22ms | 6.19x | +| `xrefs_to` | 3.79ms | 1.73ms | **2.19x** | 3.74ms | 2.16x | + +**The real-world gain is ~1.4x** (geomean over the five non-synthetic ops; 1.17x on +summed work) — NOT the 6.9x the empty round trip advertises. Quote 1.4x, not 6.9x. + +The gap between the A→C and B→C columns is the point: **we had already worked +around it**. Column B is what 0.3.2 actually fixed — stock 0.3.1 was 5.4x slower. +Upstream catching up bought us the right to *delete* ~40 lines of workaround, not +a big new win. + +Three patterns worth knowing before optimising anything here: + +| class | example | why | +|---|---|---| +| payload-dominated | `list_funcs`, `heads` | `_PACK_EPILOGUE` already fixed these (260→17.6ms); 0.3.2 adds only 1.1–1.5x | +| round-trip-dominated | `xrefs_to` (2.19x) | small payload, so the workarounds never helped — A≈B≈3.7ms. Its whole gain is the new floor | +| IDA-work-dominated | `decompile` (1.06x) | 32ms of Hex-Rays inside IDA. Nothing upstream can touch it | + +**You cannot feel any of this.** A listing page went 6.45→4.38ms, a graph +8.13→6.18ms — both far under a frame before and after. The win is maintenance. + +**Run-to-run spread is ~10% on a loaded box** (a re-run of column C gave 17.4 / +4.68 / 33.5 / 6.31 / 1.79). Treat one-decimal differences as noise, and don't +chase a "regression" under 1.2x without re-running on an idle box — `uptime` +first. The A→C ratios above survive that spread; the 1.06x on `decompile` does +not, and should be read as "unchanged". + +Historical, for reference — the old private worker vs Code Mode, taken with both +workarounds active and before 0.3.2: + +| op | worker | codemode (0.3.1) | | |---|---|---|---| | `heads` 200 rows | 2.65ms | 5.85ms | 2.2x | | `heads` expect-hit | 2.16ms | 4.61ms | 2.1x | | `disasm` 200 | 9.03ms | 5.25ms | **0.6x** | | `decompile` cold | 162.6ms | 30.7ms | **0.2x** | | `decomp_map` | 45.8ms | 47.9ms | 1.0x | -| empty round trip | ~0.07ms | **2.0ms** | the floor | +| empty round trip | ~0.07ms | **2.0ms** | the floor, now 0.29ms | -Two things dominated, and **both are now fixed UPSTREAM in ida-codemode 0.3.2**, -so neither is our problem any more: +Two things dominated that port, and **both are now fixed UPSTREAM in ida-codemode +0.3.2**, so neither is our problem any more: - `sys.settrace` — the runtime used to install a trace that returned itself, i.e. LINE tracing in every frame, making `ida_bytes.get_flags` 52x slower than @@ -229,9 +276,6 @@ and **0.97x** respectively — nothing. The settrace strip (and its only to pin encoder settings. Re-measure with `PYTHONPATH=. ~/ida-venv/bin/python experiments/bench_pack_trace.py`. -**Numbers in the table above predate 0.3.2** and were taken with both workarounds -active; treat them as historical. - **The 2ms round-trip floor is GONE as of ida-codemode 0.3.2** — it was never ours, and upstream removed it. Same-box A/B (editable checkout rolled back to `4195f21` and forward again, 200 iterations, `targets/echo`): diff --git a/experiments/bench_ops.py b/experiments/bench_ops.py new file mode 100644 index 0000000..6ed64e0 --- /dev/null +++ b/experiments/bench_ops.py @@ -0,0 +1,103 @@ +"""Time a realistic idatui operation mix against whatever ida-codemode is installed. + +The companion to `bench_pack_trace.py`: that one isolates a single workaround, +this one answers "how much faster is the whole client, on real operations". + +**It deliberately does not import anything version-specific**, so the SAME file +can measure an OLD idatui checkout (with its `sys.settrace` strip and packing +workarounds) and the current one. To compare across versions, copy it somewhere +outside the repo first -- `git checkout` of an older commit would otherwise +replace or delete it:: + + cp experiments/bench_ops.py /tmp/ + # C: current client, current library + PYTHONPATH=. ~/ida-venv/bin/python /tmp/bench_ops.py + + # B: current client against the OLD library (shows what the workarounds were for) + git -C ~/dev/ida-codemode checkout 4195f21 + PYTHONPATH=. ~/ida-venv/bin/python /tmp/bench_ops.py + + # A: the client as it SHIPPED on the old library, workarounds and all + git checkout d74b6f5 # the commit before the workaround removal + PYTHONPATH=. ~/ida-venv/bin/python /tmp/bench_ops.py + + git checkout master && git -C ~/dev/ida-codemode checkout main # ALWAYS restore + +ida-codemode is installed **editable** into both venvs, so checking that repo out +swaps the backend under the TUI with no reinstall -- which is what makes this A/B +cheap. Results for 0.3.1 vs 0.3.2 are in `.fastfeedback/SPEED.md`. +""" +from __future__ import annotations + +import argparse +import os +import statistics +import time + +from idatui.codemode_client import CodeModeClient + + +def bench(fn, reps: int) -> tuple[float, float]: + """Best-of and median wall time in ms; best-of resists co-tenant noise.""" + samples = [] + for _ in range(reps): + started = time.perf_counter() + fn() + samples.append((time.perf_counter() - started) * 1000.0) + return min(samples), statistics.median(samples) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("target", nargs="?", default="targets/bash") + ap.add_argument("--reps", type=int, default=20) + args = ap.parse_args() + + client = CodeModeClient(os.path.abspath(args.target)) + client.connect() + handle = client._handle + + # Work on the biggest function we can find, so the payload-heavy operations + # are actually payload-heavy. + index = client.invoke("list_funcs", queries=[{"offset": 0, "count": 60}]) + funcs = (index.get("result") or [{}])[0].get("data") or [] + if not funcs: + print("VERDICT: FAIL - no functions") + return 1 + big = max(funcs, key=lambda f: f.get("size") or 0) + ea = big["addr"] if isinstance(big["addr"], str) else hex(big["addr"]) + + ops = [ + # Synthetic: isolates the per-operation floor (execute_sync marshalling). + ("empty round trip", lambda: handle.execute_python("result = 1")), + # Payload-dominated: what _PACK_EPILOGUE was written for. + ("list_funcs 500", lambda: client.invoke( + "list_funcs", queries=[{"offset": 0, "count": 500}])), + ("heads 200 (listing page)", lambda: client.invoke( + "heads", addr=ea, count=200, annotate=True)), + # IDA-work-dominated: Hex-Rays, nothing upstream can move. + ("decompile (warm)", lambda: client.invoke("decompile", addr=ea)), + ("flowchart (graph)", lambda: client.invoke("flowchart", addr=ea)), + # Round-trip-dominated: small payload, so only the floor matters. + ("xrefs_to", lambda: client.invoke("xref_query", direction="to", addr=ea)), + ] + + print(f"# target={os.path.basename(args.target)} func={ea} reps={args.reps} " + f"backend={client.backend}") + results = {} + for name, fn in ops: + try: + for _ in range(3): # warm caches; the first sample is always an outlier + fn() + best, med = bench(fn, args.reps) + results[name] = med + print(f"{name:28} best {best:8.3f}ms median {med:8.3f}ms") + except Exception as exc: # one broken op must not lose the other five + print(f"{name:28} FAILED: {type(exc).__name__}: {str(exc)[:60]}") + client.close() + print("RESULT " + ";".join(f"{k}={v:.3f}" for k, v in results.items())) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) |
